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
|
|
@ -45,6 +45,30 @@ frames = 6
|
|||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
|
||||
[[template]]
|
||||
id = "knowledge-card-vertical"
|
||||
scene = "card"
|
||||
title_key = "sceneTemplate.item.knowledgeCardVertical.title"
|
||||
title_fallback = "知识卡片 · 竖版"
|
||||
summary_key = "sceneTemplate.item.knowledgeCardVertical.summary"
|
||||
summary_fallback = "3:4 单张图文卡,标题、四条要点和署名条,换掉文案就能发小红书。"
|
||||
tags = ["卡片", "知识", "小红书", "图文", "card", "note"]
|
||||
frames = 1
|
||||
frame_width = 1080
|
||||
frame_height = 1440
|
||||
|
||||
[[template]]
|
||||
id = "knowledge-card-square"
|
||||
scene = "card"
|
||||
title_key = "sceneTemplate.item.knowledgeCardSquare.title"
|
||||
title_fallback = "知识卡片 · 方版"
|
||||
summary_key = "sceneTemplate.item.knowledgeCardSquare.summary"
|
||||
summary_fallback = "1:1 方形卡,同一套版式的紧凑版,适合公众号头图与朋友圈。"
|
||||
tags = ["卡片", "知识", "方形", "公众号", "card", "square"]
|
||||
frames = 1
|
||||
frame_width = 1080
|
||||
frame_height = 1080
|
||||
|
||||
[[template]]
|
||||
id = "before-after"
|
||||
scene = "comparison"
|
||||
|
|
@ -56,3 +80,51 @@ tags = ["对比", "前后", "复盘", "comparison", "before after"]
|
|||
frames = 1
|
||||
frame_width = 1600
|
||||
frame_height = 900
|
||||
|
||||
[[template]]
|
||||
id = "pitch-deck-dark"
|
||||
scene = "slides"
|
||||
title_key = "sceneTemplate.item.pitchDeckDark.title"
|
||||
title_fallback = "路演 deck · 深色"
|
||||
summary_key = "sceneTemplate.item.pitchDeckDark.summary"
|
||||
summary_fallback = "封面、问题、方案、数据、里程碑和联系页,深底大字,适合融资路演与产品发布。"
|
||||
tags = ["路演", "融资", "创业", "深色", "PPT", "pitch", "deck", "startup", "dark"]
|
||||
frames = 6
|
||||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
|
||||
[[template]]
|
||||
id = "lecture-deck-light"
|
||||
scene = "slides"
|
||||
title_key = "sceneTemplate.item.lectureDeckLight.title"
|
||||
title_fallback = "课件 deck · 浅色"
|
||||
summary_key = "sceneTemplate.item.lectureDeckLight.summary"
|
||||
summary_fallback = "课程封面、学习目标、概念讲解、例题、对比表和小结作业,纸白底耐看,适合上课投影。"
|
||||
tags = ["课件", "教学", "讲课", "教育", "PPT", "lecture", "teaching", "course", "light"]
|
||||
frames = 6
|
||||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
|
||||
[[template]]
|
||||
id = "minimal-keynote"
|
||||
scene = "slides"
|
||||
title_key = "sceneTemplate.item.minimalKeynote.title"
|
||||
title_fallback = "极简 Keynote"
|
||||
summary_key = "sceneTemplate.item.minimalKeynote.summary"
|
||||
summary_fallback = "纯白留白、超大字号、一页一个意思,八页里没有一张卡片,适合发布会与主题演讲。"
|
||||
tags = ["极简", "留白", "发布会", "演讲", "keynote", "minimal", "PPT", "slides"]
|
||||
frames = 8
|
||||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
|
||||
[[template]]
|
||||
id = "gradient-tech"
|
||||
scene = "slides"
|
||||
title_key = "sceneTemplate.item.gradientTech.title"
|
||||
title_fallback = "渐变科技风"
|
||||
summary_key = "sceneTemplate.item.gradientTech.summary"
|
||||
summary_fallback = "深色渐变底加玻璃拟态卡,含架构、性能对比与客户墙,适合开发者产品发布。"
|
||||
tags = ["渐变", "科技", "玻璃", "深色", "开发者", "gradient", "tech", "glass", "PPT"]
|
||||
frames = 6
|
||||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
|
|
|
|||
2838
crates/op-editor-core/assets/scene_templates/gradient-tech.op
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"name": "知识卡片 · 方版 1:1",
|
||||
"variables": {
|
||||
"c-bg": {
|
||||
"type": "color",
|
||||
"value": "#FFF7F0"
|
||||
},
|
||||
"c-surface": {
|
||||
"type": "color",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
"c-ink": {
|
||||
"type": "color",
|
||||
"value": "#241A12"
|
||||
},
|
||||
"c-muted": {
|
||||
"type": "color",
|
||||
"value": "#7A6455"
|
||||
},
|
||||
"c-accent": {
|
||||
"type": "color",
|
||||
"value": "#C8502B"
|
||||
},
|
||||
"c-accent-soft": {
|
||||
"type": "color",
|
||||
"value": "#FAE3D8"
|
||||
},
|
||||
"c-border": {
|
||||
"type": "color",
|
||||
"value": "#EDDCD0"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f74",
|
||||
"name": "知识卡片 · 方版",
|
||||
"width": 1080,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
88,
|
||||
84
|
||||
],
|
||||
"gap": 0,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f46",
|
||||
"name": "卡头",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 26,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f41",
|
||||
"name": "标签",
|
||||
"width": "fit_content",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"padding": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"gap": 0,
|
||||
"cornerRadius": 999,
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent-soft"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t42",
|
||||
"name": "标签文字",
|
||||
"content": "学习方法",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.4,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t43",
|
||||
"name": "主标题",
|
||||
"content": "读完就忘?\n先补上这一步",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 76,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.15,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r44",
|
||||
"name": "强调短线",
|
||||
"width": 112,
|
||||
"height": 10,
|
||||
"cornerRadius": 5,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t45",
|
||||
"name": "副标题",
|
||||
"content": "三个动作,把书里的东西真正带走。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f65",
|
||||
"name": "要点列表",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f47",
|
||||
"name": "要点 1",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 22,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f48",
|
||||
"name": "序号 1",
|
||||
"width": 52,
|
||||
"height": 52,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 26,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t49",
|
||||
"name": "序号数字",
|
||||
"content": "1",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f52",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t50",
|
||||
"name": "要点标题",
|
||||
"content": "先提问,再翻开",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 34,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t51",
|
||||
"name": "要点说明",
|
||||
"content": "带着问题进去,信息才有地方落。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f53",
|
||||
"name": "要点 2",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 22,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f54",
|
||||
"name": "序号 2",
|
||||
"width": 52,
|
||||
"height": 52,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 26,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t55",
|
||||
"name": "序号数字",
|
||||
"content": "2",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f58",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t56",
|
||||
"name": "要点标题",
|
||||
"content": "合上书,先复述",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 34,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t57",
|
||||
"name": "要点说明",
|
||||
"content": "讲不出来的地方,就是还没懂的地方。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f59",
|
||||
"name": "要点 3",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 22,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f60",
|
||||
"name": "序号 3",
|
||||
"width": 52,
|
||||
"height": 52,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 26,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t61",
|
||||
"name": "序号数字",
|
||||
"content": "3",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f64",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t62",
|
||||
"name": "要点标题",
|
||||
"content": "两天之内用一次",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 34,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t63",
|
||||
"name": "要点说明",
|
||||
"content": "用过一次,才算真的学过。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f73",
|
||||
"name": "署名",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 22,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r72",
|
||||
"name": "分隔线",
|
||||
"width": "fill_container",
|
||||
"height": 2,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-border"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f68",
|
||||
"name": "署名行",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 22,
|
||||
"alignItems": "center",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f66",
|
||||
"name": "头像占位",
|
||||
"width": 76,
|
||||
"height": 76,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 38,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent-soft"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t67",
|
||||
"name": "头像文字",
|
||||
"content": "笔",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 32,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f71",
|
||||
"name": "署名文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 6,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t69",
|
||||
"name": "账号名",
|
||||
"content": "@ 你的账号名",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t70",
|
||||
"name": "一句话简介",
|
||||
"content": "每周更新一条可落地的学习方法",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 23,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"name": "知识卡片 · 竖版 3:4",
|
||||
"variables": {
|
||||
"c-bg": {
|
||||
"type": "color",
|
||||
"value": "#FFF7F0"
|
||||
},
|
||||
"c-surface": {
|
||||
"type": "color",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
"c-ink": {
|
||||
"type": "color",
|
||||
"value": "#241A12"
|
||||
},
|
||||
"c-muted": {
|
||||
"type": "color",
|
||||
"value": "#7A6455"
|
||||
},
|
||||
"c-accent": {
|
||||
"type": "color",
|
||||
"value": "#C8502B"
|
||||
},
|
||||
"c-accent-soft": {
|
||||
"type": "color",
|
||||
"value": "#FAE3D8"
|
||||
},
|
||||
"c-border": {
|
||||
"type": "color",
|
||||
"value": "#EDDCD0"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f40",
|
||||
"name": "知识卡片 · 竖版",
|
||||
"width": 1080,
|
||||
"height": 1440,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
96,
|
||||
88
|
||||
],
|
||||
"gap": 0,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f6",
|
||||
"name": "卡头",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 32,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f1",
|
||||
"name": "标签",
|
||||
"width": "fit_content",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"padding": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"gap": 0,
|
||||
"cornerRadius": 999,
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent-soft"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t2",
|
||||
"name": "标签文字",
|
||||
"content": "学习方法 · 01",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.4,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t3",
|
||||
"name": "主标题",
|
||||
"content": "为什么你读了很多书\n却什么都记不住",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 88,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.15,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r4",
|
||||
"name": "强调短线",
|
||||
"width": 132,
|
||||
"height": 12,
|
||||
"cornerRadius": 6,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t5",
|
||||
"name": "副标题",
|
||||
"content": "不是记性差,是你少做了这一步。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f31",
|
||||
"name": "要点列表",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 28,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f7",
|
||||
"name": "要点 1",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f8",
|
||||
"name": "序号 1",
|
||||
"width": 56,
|
||||
"height": 56,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 28,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t9",
|
||||
"name": "序号数字",
|
||||
"content": "1",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f12",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t10",
|
||||
"name": "要点标题",
|
||||
"content": "先提问,再翻开",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 36,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t11",
|
||||
"name": "要点说明",
|
||||
"content": "带着「我想解决什么」进去,信息才有地方落。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 27,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f13",
|
||||
"name": "要点 2",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f14",
|
||||
"name": "序号 2",
|
||||
"width": 56,
|
||||
"height": 56,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 28,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t15",
|
||||
"name": "序号数字",
|
||||
"content": "2",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f18",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t16",
|
||||
"name": "要点标题",
|
||||
"content": "合上书,先复述",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 36,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t17",
|
||||
"name": "要点说明",
|
||||
"content": "读完立刻用自己的话讲一遍,卡壳的地方就是没懂的地方。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 27,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f19",
|
||||
"name": "要点 3",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f20",
|
||||
"name": "序号 3",
|
||||
"width": 56,
|
||||
"height": 56,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 28,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t21",
|
||||
"name": "序号数字",
|
||||
"content": "3",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f24",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t22",
|
||||
"name": "要点标题",
|
||||
"content": "一次只带走三条",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 36,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t23",
|
||||
"name": "要点说明",
|
||||
"content": "贪多必忘。记住三个能用的点,比划满一整本有用。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 27,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f25",
|
||||
"name": "要点 4",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f26",
|
||||
"name": "序号 4",
|
||||
"width": 56,
|
||||
"height": 56,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 28,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t27",
|
||||
"name": "序号数字",
|
||||
"content": "4",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f30",
|
||||
"name": "要点文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t28",
|
||||
"name": "要点标题",
|
||||
"content": "两天之内用一次",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 36,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t29",
|
||||
"name": "要点说明",
|
||||
"content": "写进笔记、讲给朋友、或者直接做一遍,用过才算学过。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 27,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f39",
|
||||
"name": "署名",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 24,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r38",
|
||||
"name": "分隔线",
|
||||
"width": "fill_container",
|
||||
"height": 2,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-border"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f34",
|
||||
"name": "署名行",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"alignItems": "center",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f32",
|
||||
"name": "头像占位",
|
||||
"width": 88,
|
||||
"height": 88,
|
||||
"layout": "horizontal",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"cornerRadius": 44,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent-soft"
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t33",
|
||||
"name": "头像文字",
|
||||
"content": "笔",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 37,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.0,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f37",
|
||||
"name": "署名文案",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 6,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t35",
|
||||
"name": "账号名",
|
||||
"content": "@ 你的账号名",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t36",
|
||||
"name": "一句话简介",
|
||||
"content": "每周更新一条可落地的学习方法",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 24,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
2671
crates/op-editor-core/assets/scene_templates/lecture-deck-light.op
Normal file
641
crates/op-editor-core/assets/scene_templates/minimal-keynote.op
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"name": "极简 Keynote · 16:9",
|
||||
"variables": {
|
||||
"c-bg": {
|
||||
"type": "color",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
"c-ink": {
|
||||
"type": "color",
|
||||
"value": "#0A0A0A"
|
||||
},
|
||||
"c-muted": {
|
||||
"type": "color",
|
||||
"value": "#6B6B6B"
|
||||
},
|
||||
"c-accent": {
|
||||
"type": "color",
|
||||
"value": "#C7340F"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f8",
|
||||
"name": "01 封面",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 48,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f4",
|
||||
"name": "主张",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"gap": 40,
|
||||
"alignItems": "start",
|
||||
"fill": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t1",
|
||||
"name": "主标题",
|
||||
"content": "把复杂\n留在里面",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 130,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.08,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r2",
|
||||
"name": "强调短线",
|
||||
"width": 120,
|
||||
"height": 8,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t3",
|
||||
"name": "副标题",
|
||||
"content": "第七代产品,今天发布。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 32,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.45,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f7",
|
||||
"name": "落款",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"gap": 28,
|
||||
"alignItems": "center",
|
||||
"fill": [],
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t5",
|
||||
"name": "场合",
|
||||
"content": "2026 秋季发布会",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 500,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t6",
|
||||
"name": "日期",
|
||||
"content": "2026.10.08",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 26,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.6,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "auto",
|
||||
"width": "fit_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f9",
|
||||
"name": "02 论点",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 44,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r10",
|
||||
"name": "强调点",
|
||||
"width": 18,
|
||||
"height": 18,
|
||||
"cornerRadius": 9,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t11",
|
||||
"name": "论点",
|
||||
"content": "我们删掉了三十七个按钮。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 76,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.15,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 2040,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f12",
|
||||
"name": "03 词 · 少",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 36,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t13",
|
||||
"name": "大词",
|
||||
"content": "少",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 200,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.05,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t14",
|
||||
"name": "注解",
|
||||
"content": "从三十七个按钮,到三个。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 4080,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f15",
|
||||
"name": "04 词 · 轻",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 36,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t16",
|
||||
"name": "大词",
|
||||
"content": "轻",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 200,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.05,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t17",
|
||||
"name": "注解",
|
||||
"content": "整机一点零九公斤,比上一代少了两百四十克。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 1440
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f18",
|
||||
"name": "05 词 · 久",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 36,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t19",
|
||||
"name": "大词",
|
||||
"content": "久",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 200,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.05,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t20",
|
||||
"name": "注解",
|
||||
"content": "一次充电,用满两天。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 2040,
|
||||
"y": 1440
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f21",
|
||||
"name": "06 引用",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 32,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t22",
|
||||
"name": "引号",
|
||||
"content": "“",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 160,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
],
|
||||
"lineHeight": 0.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t23",
|
||||
"name": "引文",
|
||||
"content": "最好的设计,是让人忘了它存在。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 54,
|
||||
"fontWeight": 600,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.3,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t24",
|
||||
"name": "出处",
|
||||
"content": "— 产品负责人 沈亦文",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 28,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 4080,
|
||||
"y": 1440
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f25",
|
||||
"name": "07 数据",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 32,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t26",
|
||||
"name": "大数字",
|
||||
"content": "0.4 秒",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 200,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.05,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t27",
|
||||
"name": "注解",
|
||||
"content": "从按下到出图的全部时间,上一代是 2.6 秒。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 32,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 2880
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "f28",
|
||||
"name": "08 结尾",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"layout": "vertical",
|
||||
"padding": [
|
||||
160,
|
||||
160
|
||||
],
|
||||
"gap": 40,
|
||||
"justifyContent": "center",
|
||||
"alignItems": "start",
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-bg"
|
||||
}
|
||||
],
|
||||
"clipContent": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "r29",
|
||||
"name": "强调短线",
|
||||
"width": 120,
|
||||
"height": 8,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-accent"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t30",
|
||||
"name": "结语",
|
||||
"content": "今天开始发货。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 96,
|
||||
"fontWeight": 700,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-ink"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.12,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "t31",
|
||||
"name": "补充",
|
||||
"content": "全国门店同步开售,官网可预约体验。",
|
||||
"fontFamily": "Noto Sans SC",
|
||||
"fontSize": 30,
|
||||
"fontWeight": 400,
|
||||
"fill": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "$c-muted"
|
||||
}
|
||||
],
|
||||
"lineHeight": 1.5,
|
||||
"letterSpacing": 0,
|
||||
"textGrowth": "fixed-width",
|
||||
"width": "fill_container"
|
||||
}
|
||||
],
|
||||
"x": 2040,
|
||||
"y": 2880
|
||||
}
|
||||
]
|
||||
}
|
||||
2374
crates/op-editor-core/assets/scene_templates/pitch-deck-dark.op
Normal file
|
|
@ -89,9 +89,15 @@ pub fn scenario_default_export_format(scenario: Option<TemplateScene>) -> Option
|
|||
// A deck is delivered as a slide-per-page PDF — see
|
||||
// `op_host_services::export_pdf::export_deck_pdf`.
|
||||
Some(TemplateScene::Slides) => Some(ExportFormat::Pdf),
|
||||
// A card is delivered as a PNG, but so is a tutorial and a carousel,
|
||||
// and the user's own last choice (WebP for a lighter upload, say) is
|
||||
// as valid. Presetting here would silently overwrite that on every
|
||||
// export; the deck is the only scenario with a format its delivery
|
||||
// actually depends on.
|
||||
Some(TemplateScene::Tutorial)
|
||||
| Some(TemplateScene::Comparison)
|
||||
| Some(TemplateScene::Carousel)
|
||||
| Some(TemplateScene::Card)
|
||||
| None => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ const SCREENSHOT_TUTORIAL_OP: &str =
|
|||
const KNOWLEDGE_CAROUSEL_OP: &str = include_str!("../assets/scene_templates/knowledge-carousel.op");
|
||||
const BEFORE_AFTER_OP: &str = include_str!("../assets/scene_templates/before-after.op");
|
||||
const SLIDE_DECK_OP: &str = include_str!("../assets/scene_templates/slide-deck.op");
|
||||
const KNOWLEDGE_CARD_VERTICAL_OP: &str =
|
||||
include_str!("../assets/scene_templates/knowledge-card-vertical.op");
|
||||
const KNOWLEDGE_CARD_SQUARE_OP: &str =
|
||||
include_str!("../assets/scene_templates/knowledge-card-square.op");
|
||||
const PITCH_DECK_DARK_OP: &str = include_str!("../assets/scene_templates/pitch-deck-dark.op");
|
||||
const LECTURE_DECK_LIGHT_OP: &str = include_str!("../assets/scene_templates/lecture-deck-light.op");
|
||||
const MINIMAL_KEYNOTE_OP: &str = include_str!("../assets/scene_templates/minimal-keynote.op");
|
||||
const GRADIENT_TECH_OP: &str = include_str!("../assets/scene_templates/gradient-tech.op");
|
||||
|
||||
/// Return the embedded document JSON for a template id.
|
||||
///
|
||||
|
|
@ -40,6 +48,12 @@ pub fn scene_template_document(template_id: &str) -> Option<&'static str> {
|
|||
"knowledge-carousel" => Some(KNOWLEDGE_CAROUSEL_OP),
|
||||
"before-after" => Some(BEFORE_AFTER_OP),
|
||||
"slide-deck" => Some(SLIDE_DECK_OP),
|
||||
"knowledge-card-vertical" => Some(KNOWLEDGE_CARD_VERTICAL_OP),
|
||||
"knowledge-card-square" => Some(KNOWLEDGE_CARD_SQUARE_OP),
|
||||
"pitch-deck-dark" => Some(PITCH_DECK_DARK_OP),
|
||||
"lecture-deck-light" => Some(LECTURE_DECK_LIGHT_OP),
|
||||
"minimal-keynote" => Some(MINIMAL_KEYNOTE_OP),
|
||||
"gradient-tech" => Some(GRADIENT_TECH_OP),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -55,14 +69,22 @@ pub enum TemplateScene {
|
|||
Carousel,
|
||||
/// Presentation decks.
|
||||
Slides,
|
||||
/// Single-board social cards — one finished image, not a sequence.
|
||||
///
|
||||
/// Distinct from [`Self::Carousel`]: a carousel's unit of meaning is the
|
||||
/// swipe across several boards, so its templates ship a whole sequence. A
|
||||
/// card is delivered on its own, which is what makes "title + points +
|
||||
/// byline" a complete layout rather than a page of one.
|
||||
Card,
|
||||
}
|
||||
|
||||
impl TemplateScene {
|
||||
pub const ALL: [Self; 4] = [
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::Tutorial,
|
||||
Self::Comparison,
|
||||
Self::Carousel,
|
||||
Self::Slides,
|
||||
Self::Card,
|
||||
];
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
|
|
@ -71,6 +93,7 @@ impl TemplateScene {
|
|||
Self::Comparison => "comparison",
|
||||
Self::Carousel => "carousel",
|
||||
Self::Slides => "slides",
|
||||
Self::Card => "card",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,16 +104,23 @@ impl TemplateScene {
|
|||
Self::Comparison => "sceneTemplate.scene.comparison",
|
||||
Self::Carousel => "sceneTemplate.scene.carousel",
|
||||
Self::Slides => "sceneTemplate.scene.slides",
|
||||
Self::Card => "sceneTemplate.scene.card",
|
||||
}
|
||||
}
|
||||
|
||||
/// Label shown when the catalogue key is not yet translated.
|
||||
///
|
||||
/// Chip-sized nouns, not descriptions: the filter row is a single
|
||||
/// non-wrapping line. `Carousel` reads "轮播" rather than "知识卡片"
|
||||
/// because the latter is what a [`Self::Card`] chip means — two chips
|
||||
/// labelled the same thing is worse than a slightly drier word.
|
||||
pub const fn title_fallback(self) -> &'static str {
|
||||
match self {
|
||||
Self::Tutorial => "教程图",
|
||||
Self::Comparison => "对比图",
|
||||
Self::Carousel => "知识卡片",
|
||||
Self::Carousel => "轮播",
|
||||
Self::Slides => "PPT",
|
||||
Self::Card => "卡片",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +134,7 @@ impl FromStr for TemplateScene {
|
|||
"comparison" => Ok(Self::Comparison),
|
||||
"carousel" => Ok(Self::Carousel),
|
||||
"slides" => Ok(Self::Slides),
|
||||
"card" => Ok(Self::Card),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,22 @@ colour = "red"
|
|||
);
|
||||
}
|
||||
|
||||
/// A scene with no templates paints a chip that filters the grid to empty.
|
||||
///
|
||||
/// The scene enum and the catalogue are edited separately — adding a variant
|
||||
/// is a compiler-guided change, adding its templates is not — so nothing else
|
||||
/// stops a chip from shipping ahead of the documents it filters for.
|
||||
#[test]
|
||||
fn every_scene_has_at_least_one_template_so_no_filter_chip_is_dead() {
|
||||
for scene in TemplateScene::ALL {
|
||||
assert!(
|
||||
scene_templates_for(scene).next().is_some(),
|
||||
"scene `{}` has no templates — its filter chip would show an empty grid",
|
||||
scene.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_scene_has_a_distinct_label_so_the_filter_row_is_unambiguous() {
|
||||
let labels: Vec<&str> = TemplateScene::ALL
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -36,6 +36,12 @@ pub(crate) fn scene_template_preview(template_id: &str) -> Option<(u64, &'static
|
|||
"knowledge-carousel" => preview!(2, "knowledge-carousel"),
|
||||
"before-after" => preview!(3, "before-after"),
|
||||
"slide-deck" => preview!(4, "slide-deck"),
|
||||
"knowledge-card-vertical" => preview!(5, "knowledge-card-vertical"),
|
||||
"knowledge-card-square" => preview!(6, "knowledge-card-square"),
|
||||
"pitch-deck-dark" => preview!(7, "pitch-deck-dark"),
|
||||
"lecture-deck-light" => preview!(8, "lecture-deck-light"),
|
||||
"minimal-keynote" => preview!(9, "minimal-keynote"),
|
||||
"gradient-tech" => preview!(10, "gradient-tech"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ OUT=$R/templates/step0/previews
|
|||
TMP=$(mktemp -d)
|
||||
mkdir -p "$OUT"
|
||||
|
||||
for t in screenshot-tutorial knowledge-carousel before-after slide-deck; do
|
||||
for t in screenshot-tutorial knowledge-carousel before-after slide-deck \
|
||||
knowledge-card-vertical knowledge-card-square \
|
||||
pitch-deck-dark lecture-deck-light \
|
||||
minimal-keynote gradient-tech; do
|
||||
# The documents themselves are shipped assets (embedded by the scene
|
||||
# template catalogue); this directory keeps only the generators and the
|
||||
# full-resolution renders they produce.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,13 @@ CARDS = [
|
|||
("knowledge-carousel", "knowledge-carousel-overview.png"),
|
||||
("before-after", "before-after.png"),
|
||||
("slide-deck", [f"slide-deck-{i:02d}.png" for i in range(1, 7)]),
|
||||
("knowledge-card-vertical", "knowledge-card-vertical.png"),
|
||||
("knowledge-card-square", "knowledge-card-square.png"),
|
||||
("pitch-deck-dark", [f"pitch-deck-dark-{i:02d}.png" for i in range(1, 7)]),
|
||||
("lecture-deck-light",
|
||||
[f"lecture-deck-light-{i:02d}.png" for i in range(1, 7)]),
|
||||
("minimal-keynote", [f"minimal-keynote-{i:02d}.png" for i in range(1, 9)]),
|
||||
("gradient-tech", [f"gradient-tech-{i:02d}.png" for i in range(1, 7)]),
|
||||
]
|
||||
|
||||
# Gap between tiles, in source pixels — scaled down with everything else.
|
||||
|
|
|
|||
226
templates/step0/_generators/tpl_cards.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
"""knowledge-card-vertical.op + knowledge-card-square.op — 中文知识卡片
|
||||
|
||||
两套模板共用一份生成器,因为它们是同一套设计语言的两个画幅:竖版
|
||||
1080×1440 发小红书图文,方版 1080×1080 发公众号头图和朋友圈。分成两个
|
||||
文件写,改一处配色就得记得改另一处;合成一个,两张卡永远同源。
|
||||
|
||||
版式只做「标题 + 要点 + 署名」三段,是刻意的:卡片场景的用户要改的是
|
||||
文案,不是结构。多一个装饰区就多一处他们得先看懂再删掉的东西。
|
||||
|
||||
硬契约(与 skills 的中文排版约定一致):
|
||||
- 内容距边缘 ≥80px(竖版 88/96,方版 84/88)
|
||||
- 配色全部走 color_vars,改主色只改 $c-accent 一处
|
||||
- 正文与背景对比度 ≥2.0(本配色最低一对是 3.67,见文件末尾注释)
|
||||
- CJK 行高:大标题 1.15,小标题 1.3,正文 1.6
|
||||
- 顶层 frame 必须显式写 x/y,否则多帧会全部堆在原点
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
# 暖纸底 + 单一强调色。知识卡片是「读」的东西,底色带一点暖比纯白更耐看,
|
||||
# 也和已有的 knowledge-carousel(冷调靛蓝)拉开距离。
|
||||
VARS = color_vars({
|
||||
"c-bg": "#FFF7F0",
|
||||
"c-surface": "#FFFFFF",
|
||||
"c-ink": "#241A12",
|
||||
"c-muted": "#7A6455",
|
||||
"c-accent": "#C8502B",
|
||||
"c-accent-soft": "#FAE3D8",
|
||||
"c-border": "#EDDCD0",
|
||||
})
|
||||
|
||||
# 正文用的中文字族。oplib 的默认族按字号/字重挑(<34 且 <600 走 Inter),
|
||||
# 那条规则是为中英混排写的;这两张卡是纯中文,所以每个文本节点都显式写死
|
||||
# 中文族,免得小字号正文落到一个没有汉字的字族上再靠 fallback 兜。
|
||||
CJK = "Noto Sans SC"
|
||||
# 序号和数字用 Inter:等宽感更强,"01" 这类标号排出来才齐。
|
||||
NUM = "Inter"
|
||||
|
||||
|
||||
def card(name, *, width, height, pad_y, pad_x, children):
|
||||
"""一张卡。固定画幅,绝不 fit_content —— 发布尺寸是硬约束。
|
||||
|
||||
`space_between` 把「头部 / 要点 / 署名」三段推到各自该在的位置:
|
||||
卡片的留白不该是均匀的 gap,而是三段之间自然撑开的呼吸。
|
||||
"""
|
||||
node = frame(ids, name, width=width, height=height, layout="vertical",
|
||||
padding=[pad_y, pad_x], gap=0,
|
||||
justifyContent="space_between", alignItems="start",
|
||||
fill=solid("$c-bg"), clipContent=True)
|
||||
node["children"] = children
|
||||
node["x"] = 0
|
||||
node["y"] = 0
|
||||
return node
|
||||
|
||||
|
||||
def block(name, children, gap=32):
|
||||
"""透明结构容器。没有 fill 才是结构层;一旦有 fill 它就是一张卡面了。"""
|
||||
node = frame(ids, name, width="fill_container", height="fit_content",
|
||||
layout="vertical", gap=gap, alignItems="start", fill=[])
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def eyebrow(label, size=26):
|
||||
"""小标签。整张卡唯一的胶囊,用来交代这是哪一类内容。"""
|
||||
node = frame(ids, "标签", width="fit_content", height="fit_content",
|
||||
layout="horizontal", padding=[12, 24], gap=0,
|
||||
cornerRadius=999, alignItems="center",
|
||||
justifyContent="center", fill=solid("$c-accent-soft"))
|
||||
node["children"] = [
|
||||
text(ids, "标签文字", label, size, 600, "$c-accent", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.4),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def rule(width=132, height=12):
|
||||
return rect(ids, "强调短线", width=width, height=height,
|
||||
cornerRadius=height // 2, fill=solid("$c-accent"))
|
||||
|
||||
|
||||
def numbered(no, size):
|
||||
"""实心序号圆。白字压在主色上,是卡里对比度最高的一处,读者的视线锚点。"""
|
||||
node = frame(ids, f"序号 {no}", width=size, height=size,
|
||||
layout="horizontal", alignItems="center",
|
||||
justifyContent="center", cornerRadius=size // 2,
|
||||
fill=solid("$c-accent"))
|
||||
node["children"] = [
|
||||
text(ids, "序号数字", str(no), round(size * 0.5), 700, "#FFFFFF",
|
||||
family=NUM, width="fit_content", growth="auto", line_height=1.0),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def point(no, title, desc, *, badge, title_size, desc_size, gap):
|
||||
node = frame(ids, f"要点 {no}", width="fill_container",
|
||||
height="fit_content", layout="horizontal", gap=gap,
|
||||
alignItems="start", fill=[])
|
||||
node["children"] = [
|
||||
numbered(no, badge),
|
||||
block("要点文案", [
|
||||
text(ids, "要点标题", title, title_size, 600, "$c-ink",
|
||||
family=CJK, line_height=1.3),
|
||||
text(ids, "要点说明", desc, desc_size, 400, "$c-muted",
|
||||
family=CJK, line_height=1.6),
|
||||
], gap=10),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def signature(*, avatar, name_size, slogan_size, gap):
|
||||
"""署名条。一条细分隔线 + 头像占位 + 名字 + 一句 slogan。
|
||||
|
||||
头像用 frame 而不是 rectangle:frame 会递归渲染子节点,圆里那个字才
|
||||
显示得出来;同时 frame 也是合法的图片拖放目标,用户把自己的头像拖上去
|
||||
就直接替换掉这个占位。
|
||||
"""
|
||||
disc = frame(ids, "头像占位", width=avatar, height=avatar,
|
||||
layout="horizontal", alignItems="center",
|
||||
justifyContent="center", cornerRadius=avatar // 2,
|
||||
fill=solid("$c-accent-soft"))
|
||||
disc["children"] = [
|
||||
text(ids, "头像文字", "笔", round(avatar * 0.42), 700, "$c-accent",
|
||||
family=CJK, width="fit_content", growth="auto", line_height=1.0),
|
||||
]
|
||||
|
||||
row = frame(ids, "署名行", width="fill_container", height="fit_content",
|
||||
layout="horizontal", gap=gap, alignItems="center", fill=[])
|
||||
row["children"] = [
|
||||
disc,
|
||||
block("署名文案", [
|
||||
text(ids, "账号名", "@ 你的账号名", name_size, 600, "$c-ink",
|
||||
family=CJK, line_height=1.3),
|
||||
text(ids, "一句话简介", "每周更新一条可落地的学习方法",
|
||||
slogan_size, 400, "$c-muted", family=CJK, line_height=1.5),
|
||||
], gap=6),
|
||||
]
|
||||
|
||||
divider = rect(ids, "分隔线", width="fill_container", height=2,
|
||||
fill=solid("$c-border"))
|
||||
return block("署名", [divider, row], gap=gap)
|
||||
|
||||
|
||||
# ----------------------------------------------------------- 竖版 1080×1440
|
||||
VERTICAL_POINTS = [
|
||||
("先提问,再翻开",
|
||||
"带着「我想解决什么」进去,信息才有地方落。"),
|
||||
("合上书,先复述",
|
||||
"读完立刻用自己的话讲一遍,卡壳的地方就是没懂的地方。"),
|
||||
("一次只带走三条",
|
||||
"贪多必忘。记住三个能用的点,比划满一整本有用。"),
|
||||
("两天之内用一次",
|
||||
"写进笔记、讲给朋友、或者直接做一遍,用过才算学过。"),
|
||||
]
|
||||
|
||||
|
||||
def vertical():
|
||||
head = block("卡头", [
|
||||
eyebrow("学习方法 · 01"),
|
||||
text(ids, "主标题", "为什么你读了很多书\n却什么都记不住", 88, 700,
|
||||
"$c-ink", family=CJK, line_height=1.15),
|
||||
rule(),
|
||||
text(ids, "副标题", "不是记性差,是你少做了这一步。", 30, 400,
|
||||
"$c-muted", family=CJK, line_height=1.5),
|
||||
], gap=32)
|
||||
|
||||
points = block("要点列表", [
|
||||
point(no, title, desc, badge=56, title_size=36, desc_size=27, gap=24)
|
||||
for no, (title, desc) in enumerate(VERTICAL_POINTS, 1)
|
||||
], gap=28)
|
||||
|
||||
return card("知识卡片 · 竖版", width=1080, height=1440, pad_y=96, pad_x=88,
|
||||
children=[head, points,
|
||||
signature(avatar=88, name_size=30, slogan_size=24,
|
||||
gap=24)])
|
||||
|
||||
|
||||
# ----------------------------------------------------------- 方版 1080×1080
|
||||
SQUARE_POINTS = [
|
||||
("先提问,再翻开",
|
||||
"带着问题进去,信息才有地方落。"),
|
||||
("合上书,先复述",
|
||||
"讲不出来的地方,就是还没懂的地方。"),
|
||||
("两天之内用一次",
|
||||
"用过一次,才算真的学过。"),
|
||||
]
|
||||
|
||||
|
||||
def square():
|
||||
head = block("卡头", [
|
||||
eyebrow("学习方法"),
|
||||
text(ids, "主标题", "读完就忘?\n先补上这一步", 76, 700, "$c-ink",
|
||||
family=CJK, line_height=1.15),
|
||||
rule(112, 10),
|
||||
text(ids, "副标题", "三个动作,把书里的东西真正带走。", 28, 400,
|
||||
"$c-muted", family=CJK, line_height=1.5),
|
||||
], gap=26)
|
||||
|
||||
points = block("要点列表", [
|
||||
point(no, title, desc, badge=52, title_size=34, desc_size=26, gap=22)
|
||||
for no, (title, desc) in enumerate(SQUARE_POINTS, 1)
|
||||
], gap=24)
|
||||
|
||||
return card("知识卡片 · 方版", width=1080, height=1080, pad_y=88, pad_x=84,
|
||||
children=[head, points,
|
||||
signature(avatar=76, name_size=28, slogan_size=23,
|
||||
gap=22)])
|
||||
|
||||
|
||||
# 对比度(WCAG 相对亮度比,op-design-lint 的门槛是 2.0):
|
||||
# c-ink on c-bg 16.09 c-muted on c-bg 5.24
|
||||
# c-accent on c-bg 4.27 c-accent on c-accent-soft 3.67
|
||||
# #FFFFFF on c-accent 4.53
|
||||
# 最低一对 3.67 仍高出门槛近一倍,换主色时保留这个余量即可。
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_doc(sys.argv[1], VARS, [vertical()], "知识卡片 · 竖版 3:4")
|
||||
write_doc(sys.argv[2], VARS, [square()], "知识卡片 · 方版 1:1")
|
||||
416
templates/step0/_generators/tpl_gradient_tech.py
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
#!/usr/bin/env python3
|
||||
"""gradient-tech.op — 渐变科技风 deck(6 页 1920×1080)
|
||||
|
||||
深底 + 大面积渐变 + 玻璃拟态卡,开发者产品口吻。这套同时是引擎能力的展台:
|
||||
线性渐变、径向光晕、玻璃卡三样都是 schema 原生支持、渲染栈真画得出来的
|
||||
(渲染探针见交付说明)。
|
||||
|
||||
## 玻璃卡为什么不用真 alpha
|
||||
|
||||
玻璃拟态的自然写法是半透明白 `#FFFFFF1F`。渲染没问题,但 op-design-lint
|
||||
的对比度检测器 `parse_hex_color` 会丢掉 alpha 通道,把这张卡当成**纯白不
|
||||
透明**:卡里的白字于是被判成 1.00:1,而真实合成结果是深紫上的白字。后果
|
||||
不只是误报——它会让这张卡内的所有文字都失去有效检测,真出现低对比配色也
|
||||
照样不报。
|
||||
|
||||
所以这里的玻璃卡用「不透明渐变」:stops 是「白 @ GLASS_ALPHA 合成到页面
|
||||
渐变对应 stop」的结果,光学上与真 alpha 一致(对照渲染验证过),但不含
|
||||
alpha 通道。代价是 stops 是烤死的——改了下面的 GRADIENT 就必须重跑本生成
|
||||
器,不能只改 .op 里的变量。
|
||||
|
||||
排版遵循 skills/domains/slides.md 的硬契约:
|
||||
- 每帧固定 1920×1080,绝不 fit_content;内容距边 ≥100(这里 120)
|
||||
- 正文 ≥24(取 26-30),标题 ≥40,关键数字 80-200(数据页取 132)
|
||||
- 行高:标题 1.1-1.2,正文 1.45+
|
||||
- 最多 2 个字体族(Noto Sans SC 排中文,Inter 排数字与代码)
|
||||
- 强调色只用于强调
|
||||
|
||||
渐变底上的对比度按**渐变两端分别实测**(比单点检测更严),最低一对
|
||||
4.85: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()
|
||||
|
||||
# 页面渐变的三个 stop:深紫 → 靛 → 蓝。玻璃卡的合成色由它们算出来,所以
|
||||
# 这里是整套配色的唯一真源。
|
||||
GRADIENT = [(0.0, "#1B0B3B"), (0.55, "#2A1E6E"), (1.0, "#0A2C6E")]
|
||||
# 玻璃的白覆盖率。这个数是承重的:0.12 时强调色压在最亮那段玻璃上是
|
||||
# 4.85:1,提到 0.18 就掉到 4.00 —— 想让玻璃更亮,得先把强调色一起调。
|
||||
GLASS_ALPHA = 0.12
|
||||
GLASS_EDGE_ALPHA = 0.30
|
||||
|
||||
|
||||
def over(alpha, background):
|
||||
"""白色以 alpha 覆盖 background 后的不透明结果(直接的 source-over)。"""
|
||||
channels = [int(background[i:i + 2], 16) for i in (1, 3, 5)]
|
||||
return "#" + "".join(
|
||||
f"{round(alpha * 255 + (1 - alpha) * c):02X}" for c in channels
|
||||
)
|
||||
|
||||
|
||||
VARS = color_vars({
|
||||
"c-grad-a": GRADIENT[0][1],
|
||||
"c-grad-b": GRADIENT[1][1],
|
||||
"c-grad-c": GRADIENT[2][1],
|
||||
"c-ink": "#FFFFFF",
|
||||
"c-muted": "#C9D4F5",
|
||||
"c-accent": "#3AC8FF",
|
||||
# 对比条里「被比下去」的那根。用正文的 c-muted 会让基线条比强调条还
|
||||
# 抢眼(它更长),所以单独给一档更收的合成色;条不是文字,不参与对比
|
||||
# 度门槛。
|
||||
"c-bar-base": over(0.45, GRADIENT[1][1]),
|
||||
# 玻璃卡的三段合成色(由 GRADIENT + GLASS_ALPHA 算出,改了要重跑)。
|
||||
"c-glass-a": over(GLASS_ALPHA, GRADIENT[0][1]),
|
||||
"c-glass-b": over(GLASS_ALPHA, GRADIENT[1][1]),
|
||||
"c-glass-c": over(GLASS_ALPHA, GRADIENT[2][1]),
|
||||
# 描边不参与对比度检测,这里可以放心用真 alpha 取那道细白边。
|
||||
"c-glass-edge": "#FFFFFF4D",
|
||||
})
|
||||
|
||||
W, H = 1920, 1080
|
||||
EDGE = 120
|
||||
|
||||
CJK = "Noto Sans SC"
|
||||
NUM = "Inter"
|
||||
|
||||
# 角度 135 = 左上 → 右下(渲染实测)。
|
||||
PAGE_ANGLE = 135
|
||||
|
||||
|
||||
def page_fill():
|
||||
return [{
|
||||
"type": "linear_gradient",
|
||||
"angle": PAGE_ANGLE,
|
||||
"stops": [{"offset": offset, "color": f"$c-grad-{key}"}
|
||||
for (offset, _), key in zip(GRADIENT, "abc")],
|
||||
}]
|
||||
|
||||
|
||||
def glass_fill():
|
||||
"""玻璃卡体。见文件头:不透明渐变,不是 alpha。"""
|
||||
return [{
|
||||
"type": "linear_gradient",
|
||||
"angle": PAGE_ANGLE,
|
||||
"stops": [{"offset": offset, "color": f"$c-glass-{key}"}
|
||||
for (offset, _), key in zip(GRADIENT, "abc")],
|
||||
}]
|
||||
|
||||
|
||||
def glass_edge():
|
||||
return {"thickness": 2, "fill": solid("$c-glass-edge")}
|
||||
|
||||
|
||||
def slide(name, *, gap=56, justify="start"):
|
||||
"""一帧。固定 1920×1080,底是整页渐变。"""
|
||||
node = frame(ids, name, width=W, height=H, layout="vertical",
|
||||
padding=[EDGE, EDGE], gap=gap, justifyContent=justify,
|
||||
alignItems="start", fill=page_fill(), clipContent=True)
|
||||
node["children"] = []
|
||||
return node
|
||||
|
||||
|
||||
def col(name, children, *, gap=16, width="fill_container",
|
||||
height="fit_content", align="start", fill=None, **props):
|
||||
node = frame(ids, name, width=width, height=height, layout="vertical",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def row(name, children, *, gap=32, align="start", width="fill_container",
|
||||
height="fit_content", fill=None, **props):
|
||||
node = frame(ids, name, width=width, height=height, layout="horizontal",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def glass_card(name, children, *, gap=18, height="fit_content",
|
||||
padding=(40, 32), **props):
|
||||
return col(name, children, gap=gap, height=height,
|
||||
padding=[padding[0], padding[1]], cornerRadius=20,
|
||||
fill=glass_fill(), stroke=glass_edge(), **props)
|
||||
|
||||
|
||||
def centered_body(name, child):
|
||||
"""内容块在页标题以下的整块空间里垂直居中,页标题每页同高。"""
|
||||
return col(name, [child], gap=0, height="fill_container",
|
||||
justifyContent="center")
|
||||
|
||||
|
||||
def eyebrow(label):
|
||||
node = frame(ids, "标签", width="fit_content", height="fit_content",
|
||||
layout="horizontal", padding=[10, 22], gap=0,
|
||||
cornerRadius=999, alignItems="center",
|
||||
justifyContent="center", fill=glass_fill(),
|
||||
stroke=glass_edge())
|
||||
node["children"] = [
|
||||
text(ids, "标签文字", label, 26, 600, "$c-accent", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.4,
|
||||
spacing=1.0),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def page_head(title, subtitle=None, *, size=60):
|
||||
kids = [text(ids, "页标题", title, size, 700, "$c-ink", family=CJK,
|
||||
line_height=1.15)]
|
||||
if subtitle:
|
||||
kids.append(text(ids, "页副标题", subtitle, 30, 400, "$c-muted",
|
||||
family=CJK, line_height=1.45))
|
||||
return col("页头", kids, gap=18)
|
||||
|
||||
|
||||
def mono(name, content, size=26, color="$c-accent"):
|
||||
"""代码腔的一行。Inter 不是等宽,但整套只允许两个字族,用字重区分。"""
|
||||
return text(ids, name, content, size, 500, color, family=NUM,
|
||||
width="fit_content", growth="auto", line_height=1.5)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 01 封面
|
||||
def cover():
|
||||
lede = col("主张", [
|
||||
text(ids, "主标题", "让每一次调用\n都在 20 毫秒内返回", 96, 700,
|
||||
"$c-ink", family=CJK, line_height=1.12),
|
||||
text(ids, "副标题", "边缘推理网关 v3.0,现已开放公测。", 32, 400,
|
||||
"$c-muted", family=CJK, line_height=1.45),
|
||||
], gap=32)
|
||||
|
||||
install = glass_card("安装行", [
|
||||
mono("安装命令", "$ npm i -g edgegate && edgegate up", 28),
|
||||
], gap=0, padding=(26, 32), width="fit_content")
|
||||
|
||||
s = slide("01 封面", gap=48, justify="space_between")
|
||||
s["children"] = [eyebrow("v3.0 · 公测"), lede, install]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 02 痛点
|
||||
PAINS = [
|
||||
("冷启动吃掉了首包",
|
||||
"函数实例每次唤醒要 800ms,用户看到的却是整整一秒的白屏。"),
|
||||
("账单没法归因",
|
||||
"月底一张总额,分不清哪个模型、哪条链路花掉的。"),
|
||||
("换云就得重写",
|
||||
"每家的运行时都不一样,迁移一次等于把接入层重做一遍。"),
|
||||
]
|
||||
|
||||
|
||||
def problems():
|
||||
cards = [
|
||||
glass_card("痛点卡", [
|
||||
text(ids, "痛点序号", f"0{index}", 30, 700, "$c-accent",
|
||||
family=NUM, width="fit_content", growth="auto",
|
||||
line_height=1.1),
|
||||
text(ids, "痛点标题", title, 36, 600, "$c-ink", family=CJK,
|
||||
line_height=1.25),
|
||||
text(ids, "痛点说明", desc, 27, 400, "$c-muted", family=CJK,
|
||||
line_height=1.55),
|
||||
], gap=18, height="fill_container", padding=(44, 36))
|
||||
for index, (title, desc) in enumerate(PAINS, 1)
|
||||
]
|
||||
|
||||
s = slide("02 痛点")
|
||||
s["children"] = [
|
||||
page_head("在边缘跑推理,卡在三件事上",
|
||||
"都不是模型的问题,是它前面那一层的问题。"),
|
||||
centered_body("痛点区", row("痛点网格", cards, gap=32,
|
||||
align="stretch")),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 03 架构
|
||||
LAYERS = [
|
||||
("接入层", "Gateway", "一个 Anycast 入口,就近落到最快的可用区。"),
|
||||
("调度层", "Scheduler", "按延迟预算和单价实时选模型,超时自动降级。"),
|
||||
("运行层", "Runtime", "常驻实例池,冷启动摊薄到 3 毫秒以内。"),
|
||||
]
|
||||
|
||||
|
||||
def architecture():
|
||||
cards = []
|
||||
for index, (name, en, desc) in enumerate(LAYERS, 1):
|
||||
head = row("层头", [
|
||||
text(ids, "层名", name, 36, 600, "$c-ink", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.25),
|
||||
mono("层英文名", en, 26, "$c-accent"),
|
||||
], gap=16, align="center", width="fit_content")
|
||||
cards.append(glass_card("架构层", [
|
||||
head,
|
||||
text(ids, "层说明", desc, 27, 400, "$c-muted", family=CJK,
|
||||
line_height=1.55),
|
||||
], gap=12, padding=(32, 36)))
|
||||
|
||||
s = slide("03 架构")
|
||||
s["children"] = [
|
||||
page_head("三层,一层只做一件事",
|
||||
"自上而下,每一层都能单独替换。"),
|
||||
centered_body("架构区", col("架构堆叠", cards, gap=20)),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 04 性能
|
||||
METRICS = [
|
||||
("20", "ms", "P50 端到端", "含调度与网络"),
|
||||
("3", "ms", "冷启动", "常驻池摊薄后"),
|
||||
("62", "%", "成本下降", "同等 QPS 对比自建"),
|
||||
]
|
||||
|
||||
# 对比条:条长按真实数值成比例(越短越快),数字写在标签里。
|
||||
BAR_TRACK = 1600
|
||||
BAR_OURS_MS = 20
|
||||
BAR_BASE_MS = 210
|
||||
|
||||
|
||||
def metric_card(value, unit, label, note):
|
||||
head = row("数值", [
|
||||
text(ids, "数值文字", value, 132, 700, "$c-accent", family=NUM,
|
||||
width="fit_content", growth="auto", line_height=1.0),
|
||||
# 单位跟着数字基线走:lineHeight 1.0 下 `end` 会把小字压到基线以
|
||||
# 下,这层只有下内距的盒子把它抬回来(jian 没有真 baseline 对齐)。
|
||||
col("单位", [
|
||||
text(ids, "单位文字", unit, 40, 600, "$c-accent", family=NUM,
|
||||
width="fit_content", growth="auto", line_height=1.0),
|
||||
], gap=0, width="fit_content", padding=[0, 0, round((132 - 40) * 0.2), 0]),
|
||||
], gap=8, align="end", width="fit_content")
|
||||
|
||||
return glass_card("性能卡", [
|
||||
head,
|
||||
text(ids, "指标名", label, 30, 600, "$c-ink", family=CJK,
|
||||
line_height=1.25),
|
||||
text(ids, "指标说明", note, 26, 400, "$c-muted", family=CJK,
|
||||
line_height=1.45),
|
||||
], gap=16, height="fill_container", padding=(40, 32))
|
||||
|
||||
|
||||
def bar(label, milliseconds, color):
|
||||
width = round(BAR_TRACK * milliseconds / BAR_BASE_MS)
|
||||
return col("对比条组", [
|
||||
text(ids, "条目标签", label, 26, 500, "$c-muted", family=CJK,
|
||||
line_height=1.4),
|
||||
rect(ids, "对比条", width=width, height=16, cornerRadius=999,
|
||||
fill=solid(color)),
|
||||
], gap=10)
|
||||
|
||||
|
||||
def performance():
|
||||
bars = glass_card("延迟对比", [
|
||||
text(ids, "对比标题", "端到端延迟对比", 30, 600, "$c-ink",
|
||||
family=CJK, line_height=1.25),
|
||||
bar("本产品 · 20ms", BAR_OURS_MS, "$c-accent"),
|
||||
bar("自建方案 · 210ms", BAR_BASE_MS, "$c-bar-base"),
|
||||
], gap=20, padding=(32, 40))
|
||||
|
||||
# 三张卡与对比条合成一块「性能区」:直接摊在帧下会让页头、网格、对比
|
||||
# 卡成为三个平级兄弟,其中只有对比卡有圆角,mixed-sibling-corner-radius
|
||||
# 会照实报出来(也确实读着像手滑)。
|
||||
body = col("性能区", [
|
||||
row("性能网格", [metric_card(*entry) for entry in METRICS],
|
||||
gap=32, align="stretch"),
|
||||
bars,
|
||||
], gap=32)
|
||||
|
||||
s = slide("04 性能", gap=44)
|
||||
s["children"] = [
|
||||
page_head("数字是压测出来的,不是估的"),
|
||||
centered_body("性能居中", body),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 05 客户
|
||||
LOGO_SLOTS = 8
|
||||
|
||||
|
||||
def customers():
|
||||
# 每个格子是 frame 而不是 rectangle:frame 会递归渲染子节点(里面那行
|
||||
# 提示才显示得出来),同时它是合法的图片拖放目标,把客户 logo 拖上去
|
||||
# 就直接替换掉占位。
|
||||
slots = []
|
||||
for index in range(LOGO_SLOTS):
|
||||
slot = frame(ids, f"Logo 位 {index + 1}", width="fill_container",
|
||||
height="fill_container", layout="vertical", gap=0,
|
||||
alignItems="center", justifyContent="center",
|
||||
cornerRadius=16, fill=glass_fill(), stroke=glass_edge())
|
||||
slot["children"] = [
|
||||
text(ids, "占位提示", "拖入 Logo", 24, 500, "$c-muted",
|
||||
family=CJK, width="fit_content", growth="auto",
|
||||
line_height=1.4),
|
||||
]
|
||||
slots.append(slot)
|
||||
|
||||
grid = col("Logo 网格", [
|
||||
row("Logo 行", slots[0:4], gap=28, align="stretch",
|
||||
height="fill_container"),
|
||||
row("Logo 行", slots[4:8], gap=28, align="stretch",
|
||||
height="fill_container"),
|
||||
], gap=28, height=420)
|
||||
|
||||
s = slide("05 客户")
|
||||
s["children"] = [
|
||||
page_head("已经在生产环境上跑",
|
||||
"八家团队,日均 27 亿次调用。"),
|
||||
centered_body("Logo 区", grid),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 06 CTA
|
||||
def call_to_action():
|
||||
lede = col("结语", [
|
||||
text(ids, "结语标题", "十分钟接完\n剩下的交给我们", 84, 700,
|
||||
"$c-ink", family=CJK, line_height=1.15),
|
||||
text(ids, "结语副标题", "公测期不限调用量,接入文档一页写完。",
|
||||
30, 400, "$c-muted", family=CJK, line_height=1.45),
|
||||
], gap=30)
|
||||
|
||||
actions = row("行动区", [
|
||||
glass_card("安装卡", [
|
||||
mono("安装命令", "$ npm i -g edgegate", 28),
|
||||
], gap=0, padding=(26, 32), width="fit_content"),
|
||||
glass_card("文档卡", [
|
||||
mono("文档地址", "docs.edgegate.dev", 28, "$c-ink"),
|
||||
], gap=0, padding=(26, 32), width="fit_content"),
|
||||
], gap=24, align="center", width="fit_content")
|
||||
|
||||
s = slide("06 CTA", gap=48, justify="space_between")
|
||||
s["children"] = [eyebrow("公测开放中"), lede, actions]
|
||||
return s
|
||||
|
||||
|
||||
# 顶层 frame 必须显式写 x/y:缺省时每帧都落在原点,六页会叠成一页。纵向
|
||||
# gap 比横向多留 240,是给画布上固定在帧顶部的帧名标签让位。
|
||||
BOARD_GAP_X = 120
|
||||
BOARD_GAP_Y = BOARD_GAP_X + 240
|
||||
BOARDS_PER_ROW = 3
|
||||
|
||||
|
||||
def build():
|
||||
boards = [cover(), problems(), architecture(), performance(),
|
||||
customers(), call_to_action()]
|
||||
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):
|
||||
# 压在页面渐变上(三个 stop):
|
||||
# c-ink 18.14 / 13.91 / 13.14 c-muted 12.28 / 9.42 / 8.90
|
||||
# c-accent 9.40 / 7.21 / 6.81
|
||||
# 压在玻璃卡上(白 @0.12 合成后的三段):
|
||||
# c-ink 13.25 / 9.82 / 9.36 c-muted 8.97 / 6.65 / 6.34
|
||||
# c-accent 6.87 / 5.09 / 4.85 ← 最低一对
|
||||
# 注意 GLASS_ALPHA 是承重参数:提到 0.18,c-accent 会掉到 4.00。
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_doc(sys.argv[1], VARS, build(), "渐变科技风 · 16:9")
|
||||
431
templates/step0/_generators/tpl_lecture_light.py
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
#!/usr/bin/env python3
|
||||
"""lecture-deck-light.op — 浅色课件 deck(封面 / 目标 / 讲解 / 例题 / 对比 / 小结)
|
||||
|
||||
课件跟汇报 deck 不是同一件事:听众要跟着做笔记,所以正文比路演更密、更
|
||||
完整,序号和步骤必须一眼数得清;同时它会被投在教室最后一排也看得见的地
|
||||
方,所以字号一点不能省。米白纸底 + 墨绿,是为了长时间盯着看不刺眼。
|
||||
|
||||
排版遵循 skills/domains/slides.md 的硬契约:
|
||||
- 每帧固定 1920×1080,绝不 fit_content;内容距边缘 ≥100(这里 120)
|
||||
- 正文 ≥24(取 26-30),标题 ≥40,行高标题 1.1-1.2、正文 1.4+
|
||||
- 最多 2 个字体族(Noto Sans SC 排中文,Inter 排数字与公式)
|
||||
- 2-3 个主色 + 中性色,强调色只用于强调
|
||||
|
||||
纸白底上的对比度(WCAG,实测见文件末尾)最低一对是 5.01: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, stroke, text, write_doc
|
||||
|
||||
ids = Ids()
|
||||
|
||||
# 米白纸 + 墨绿。底色刻意不是纯白:投影时纯白底会把整间教室照亮,米白既
|
||||
# 压得住反光,也让白色卡面浮得起来(白卡 / 纸底亮度比 1.16,看得出层次)。
|
||||
VARS = color_vars({
|
||||
"c-bg": "#F2EEE2",
|
||||
"c-surface": "#FFFFFF",
|
||||
"c-ink": "#17211C",
|
||||
"c-muted": "#55635A",
|
||||
"c-accent": "#1B6B4C",
|
||||
"c-accent-soft": "#D8E9DE",
|
||||
"c-border": "#D9D2C2",
|
||||
})
|
||||
|
||||
W, H = 1920, 1080
|
||||
EDGE = 120
|
||||
|
||||
CJK = "Noto Sans SC"
|
||||
NUM = "Inter"
|
||||
|
||||
|
||||
def slide(name, *, gap=56, justify="start", fill="$c-bg"):
|
||||
"""一帧课件。固定 1920×1080 —— 投影比例是硬约束,不是建议。"""
|
||||
node = frame(ids, name, width=W, height=H, layout="vertical",
|
||||
padding=[EDGE, EDGE], gap=gap, justifyContent=justify,
|
||||
alignItems="start", fill=solid(fill), clipContent=True)
|
||||
node["children"] = []
|
||||
return node
|
||||
|
||||
|
||||
def col(name, children, *, gap=16, width="fill_container",
|
||||
height="fit_content", align="start", fill=None, **props):
|
||||
"""纵向容器。`fill=None` 才是结构层;一旦有 fill 它就是一张卡面了。"""
|
||||
node = frame(ids, name, width=width, height=height, layout="vertical",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def row(name, children, *, gap=40, align="start", width="fill_container",
|
||||
height="fit_content", fill=None, **props):
|
||||
node = frame(ids, name, width=width, height=height, layout="horizontal",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def eyebrow(label):
|
||||
"""讲次标签。整套课件唯一的胶囊,交代「这是第几讲」。"""
|
||||
node = frame(ids, "标签", width="fit_content", height="fit_content",
|
||||
layout="horizontal", padding=[10, 22], gap=0,
|
||||
cornerRadius=999, alignItems="center",
|
||||
justifyContent="center", fill=solid("$c-accent-soft"))
|
||||
node["children"] = [
|
||||
text(ids, "标签文字", label, 26, 600, "$c-accent", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.4,
|
||||
spacing=1.0),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def accent_bar(width=150, height=10):
|
||||
return rect(ids, "强调条", width=width, height=height, cornerRadius=999,
|
||||
fill=solid("$c-accent"))
|
||||
|
||||
|
||||
def numbered(no, size, *, filled=True):
|
||||
"""序号圆。白字压在墨绿上是全页对比度最高的一处,是眼睛的落点。"""
|
||||
if filled:
|
||||
fill, ink, edge = solid("$c-accent"), "#FFFFFF", None
|
||||
else:
|
||||
fill, ink, edge = solid("$c-surface"), "$c-accent", stroke("$c-accent", 3)
|
||||
node = frame(ids, f"序号 {no}", width=size, height=size,
|
||||
layout="horizontal", gap=0, alignItems="center",
|
||||
justifyContent="center", cornerRadius=size // 2, fill=fill,
|
||||
**({"stroke": edge} if edge else {}))
|
||||
node["children"] = [
|
||||
text(ids, "序号数字", str(no), round(size * 0.46), 700, ink,
|
||||
family=NUM, width="fit_content", growth="auto", line_height=1.0),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def centered_body(name, child):
|
||||
"""把内容块撑到页标题以下的整块空间里再垂直居中。
|
||||
|
||||
页标题每页固定在同一高度(重复)是课件读起来整齐的前提;正文块又不该
|
||||
被拉长到失真。两个诉求靠这一层透明容器同时满足。
|
||||
"""
|
||||
return col(name, [child], gap=0, height="fill_container",
|
||||
justifyContent="center")
|
||||
|
||||
|
||||
def bullet_dot(size=14, line_size=28, line_height=1.55):
|
||||
"""项目符号。包一层只有上内距的容器,把圆心压到首行的光学中线上。
|
||||
|
||||
`align="start"` 会把圆点顶边对齐到文本盒顶边,而文本首行的中线在
|
||||
`fontSize * lineHeight / 2` 处 —— 直接并排会让每个圆点浮在字的上方。
|
||||
"""
|
||||
lift = round(line_size * line_height / 2 - size / 2)
|
||||
return col("条目圆点位", [
|
||||
rect(ids, "条目圆点", width=size, height=size,
|
||||
cornerRadius=size // 2, fill=solid("$c-accent")),
|
||||
], gap=0, width="fit_content", padding=[lift, 0, 0, 0])
|
||||
|
||||
|
||||
def page_head(title, subtitle=None, *, size=64):
|
||||
kids = [text(ids, "页标题", title, size, 700, "$c-ink", family=CJK,
|
||||
line_height=1.15)]
|
||||
if subtitle:
|
||||
kids.append(text(ids, "页副标题", subtitle, 30, 400, "$c-muted",
|
||||
family=CJK, line_height=1.45))
|
||||
return col("页头", kids, gap=18)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 01 封面
|
||||
def cover():
|
||||
lede = col("课程标题", [
|
||||
text(ids, "课名", "函数的单调性\n与极值判定", 100, 700, "$c-ink",
|
||||
family=CJK, line_height=1.12),
|
||||
accent_bar(),
|
||||
text(ids, "课程说明", "这一讲我们只解决一个问题:拿到一个函数,怎么"
|
||||
"判断它在哪段往上走。", 32, 400, "$c-muted",
|
||||
family=CJK, line_height=1.45),
|
||||
], gap=36)
|
||||
|
||||
meta = row("讲师信息", [
|
||||
text(ids, "讲师", "主讲 · 林望舒", 28, 500, "$c-ink", family=CJK,
|
||||
width="fit_content", growth="auto"),
|
||||
text(ids, "学期", "高等数学(上)· 2026 秋", 28, 400, "$c-muted",
|
||||
family=CJK, width="fit_content", growth="auto"),
|
||||
], gap=32, align="center", justifyContent="space_between")
|
||||
|
||||
s = slide("01 课程封面", gap=48, justify="space_between")
|
||||
s["children"] = [eyebrow("第 07 讲"), lede, meta]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 02 目标
|
||||
OBJECTIVES = [
|
||||
("说得出定义", "用导数的符号说明单调递增、递减到底在讲什么。"),
|
||||
("判得出区间", "给定一个可导函数,能划出它的单调区间。"),
|
||||
("找得到极值", "会用一阶导数的符号变化确定极大值与极小值。"),
|
||||
]
|
||||
|
||||
|
||||
def objectives():
|
||||
items = []
|
||||
for index, (title, desc) in enumerate(OBJECTIVES, 1):
|
||||
items.append(row(f"目标 {index}", [
|
||||
numbered(index, 64),
|
||||
col("目标文案", [
|
||||
text(ids, "目标标题", title, 38, 600, "$c-ink", family=CJK,
|
||||
line_height=1.25),
|
||||
text(ids, "目标说明", desc, 28, 400, "$c-muted", family=CJK,
|
||||
line_height=1.5),
|
||||
], gap=12),
|
||||
], gap=28, align="start", padding=[36, 40], cornerRadius=20,
|
||||
fill=solid("$c-surface")))
|
||||
|
||||
s = slide("02 学习目标", gap=56)
|
||||
s["children"] = [
|
||||
page_head("这节课下课时,你应该能——",
|
||||
"三条,都要能自己动手做出来,不是听懂就算。"),
|
||||
centered_body("目标区", col("目标列表", items, gap=24)),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 03 讲解
|
||||
def concept():
|
||||
body = col("概念正文", [
|
||||
col("定义组", [
|
||||
text(ids, "定义标题", "单调性的判定定理", 40, 600, "$c-ink",
|
||||
family=CJK, line_height=1.25),
|
||||
text(ids, "定义正文",
|
||||
"设函数 f(x) 在区间 (a, b) 内可导。若在该区间内恒有 "
|
||||
"f'(x) > 0,则 f(x) 在 (a, b) 上单调递增;若恒有 "
|
||||
"f'(x) < 0,则单调递减。",
|
||||
30, 400, "$c-ink", family=CJK, line_height=1.6),
|
||||
], gap=20),
|
||||
col("追问组", [
|
||||
rect(ids, "正文分隔线", width="fill_container", height=2,
|
||||
fill=solid("$c-border")),
|
||||
text(ids, "提示标题", "为什么成立", 30, 600, "$c-accent",
|
||||
family=CJK, line_height=1.3),
|
||||
text(ids, "提示正文",
|
||||
"导数是切线的斜率。斜率一直为正,意味着曲线上任意两点连线"
|
||||
"都在往右上方走——这正是「递增」的几何说法。",
|
||||
28, 400, "$c-muted", family=CJK, line_height=1.6),
|
||||
], gap=20),
|
||||
], gap=40, height="fill_container", justifyContent="space_between",
|
||||
padding=[44, 44], cornerRadius=20, fill=solid("$c-surface"))
|
||||
|
||||
# 配图位用 frame 而不是 rectangle:frame 会递归渲染子节点,里面那两行
|
||||
# 提示才显示得出来;frame 同时是合法的图片拖放目标,把函数图像拖上去
|
||||
# 就直接替换掉这个占位。
|
||||
figure = col("配图占位", [
|
||||
text(ids, "配图提示", "把函数图像拖到这里", 30, 600, "$c-accent",
|
||||
family=CJK, width="fit_content", growth="auto",
|
||||
line_height=1.3),
|
||||
text(ids, "配图说明", "建议画出 f(x) 与 f'(x) 的符号对照",
|
||||
26, 400, "$c-muted", family=CJK, width="fit_content",
|
||||
growth="auto", line_height=1.45),
|
||||
], gap=12, width=700, height="fill_container", padding=[0, 48],
|
||||
align="center", justifyContent="center", cornerRadius=20,
|
||||
fill=solid("$c-accent-soft"))
|
||||
|
||||
s = slide("03 概念讲解", gap=56)
|
||||
s["children"] = [
|
||||
page_head("导数的符号,决定曲线的走向"),
|
||||
centered_body("图文区", row("图文两栏", [body, figure], gap=48,
|
||||
align="stretch", height=560)),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 04 例题
|
||||
STEPS = [
|
||||
("求导", "f'(x) = 3x² − 6x = 3x(x − 2)"),
|
||||
("找驻点", "令 f'(x) = 0,得 x = 0 与 x = 2"),
|
||||
("判符号", "x < 0 与 x > 2 时 f'(x) > 0;0 < x < 2 时 f'(x) < 0"),
|
||||
("下结论", "x = 0 处取极大值 f(0) = 1,x = 2 处取极小值 f(2) = −3"),
|
||||
]
|
||||
|
||||
|
||||
def worked_example():
|
||||
prompt = col("题干", [
|
||||
text(ids, "题干标签", "例题", 26, 600, "$c-accent", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.3,
|
||||
spacing=1.0),
|
||||
text(ids, "题干正文",
|
||||
"求函数 f(x) = x³ − 3x² + 1 的单调区间与极值。",
|
||||
34, 600, "$c-ink", family=CJK, line_height=1.35),
|
||||
], gap=12, padding=[32, 36], cornerRadius=16, fill=solid("$c-accent-soft"))
|
||||
|
||||
steps = []
|
||||
for index, (title, formula) in enumerate(STEPS, 1):
|
||||
steps.append(row(f"步骤 {index}", [
|
||||
numbered(index, 52),
|
||||
col("步骤文案", [
|
||||
text(ids, "步骤标题", title, 30, 600, "$c-ink", family=CJK,
|
||||
line_height=1.3),
|
||||
text(ids, "步骤算式", formula, 28, 400, "$c-muted",
|
||||
family=NUM, line_height=1.5),
|
||||
], gap=8),
|
||||
], gap=24, align="start"))
|
||||
|
||||
# 题干与解答装进同一张卡:例题在课件里是一个整体,拆成两块平级容器既
|
||||
# 削弱了「这是一道题」的读法,也让页面上出现三个圆角不一致的兄弟
|
||||
# (mixed-sibling-corner-radius 会照实报出来)。
|
||||
exam = col("例题卡", [
|
||||
prompt,
|
||||
col("解答步骤", steps, gap=24),
|
||||
], gap=32, padding=[40, 44], cornerRadius=20, fill=solid("$c-surface"))
|
||||
|
||||
s = slide("04 例题", gap=56)
|
||||
s["children"] = [
|
||||
page_head("按这四步走,一步都不要跳"),
|
||||
centered_body("例题区", exam),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 05 对比
|
||||
COMPARISON = [
|
||||
("看的是什么", "f'(x) 在整个区间上的符号", "f'(x) 在某一点两侧的符号变化"),
|
||||
("回答的问题", "这段曲线往上还是往下", "这一点是不是拐上去/拐下来的地方"),
|
||||
("容易错在哪", "漏掉不可导的点也要单独讨论",
|
||||
"驻点不一定是极值点,要看符号是否真的变了"),
|
||||
]
|
||||
|
||||
# 表格分隔线用 PenStroke 的 per-side 厚度:只画底边,四条边全画会把表格
|
||||
# 变成田字格,行与行的关系反而看不出来。最后一行不画,免得表格底部多出
|
||||
# 一条没有下文的线。
|
||||
def table_row(label, left, right, *, last=False):
|
||||
cells = [
|
||||
col("行标题", [
|
||||
text(ids, "行标题文字", label, 27, 600, "$c-muted", family=CJK,
|
||||
line_height=1.4),
|
||||
], gap=0, width=260),
|
||||
col("左列内容", [
|
||||
text(ids, "左列文字", left, 28, 400, "$c-ink", family=CJK,
|
||||
line_height=1.5),
|
||||
], gap=0),
|
||||
col("右列内容", [
|
||||
text(ids, "右列文字", right, 28, 400, "$c-ink", family=CJK,
|
||||
line_height=1.5),
|
||||
], gap=0),
|
||||
]
|
||||
props = {"padding": [26, 32]}
|
||||
if not last:
|
||||
props["stroke"] = {"thickness": {"bottom": 2},
|
||||
"fill": solid("$c-border")}
|
||||
return row("对比行", cells, gap=40, align="start", **props)
|
||||
|
||||
|
||||
def comparison():
|
||||
header = row("表头", [
|
||||
col("表头行标题", [
|
||||
text(ids, "表头留白", " ", 27, 600, "$c-muted", family=CJK,
|
||||
line_height=1.4),
|
||||
], gap=0, width=260),
|
||||
col("表头左列", [
|
||||
text(ids, "表头左列文字", "单调性", 34, 700, "$c-accent",
|
||||
family=CJK, line_height=1.3),
|
||||
], gap=0),
|
||||
col("表头右列", [
|
||||
text(ids, "表头右列文字", "极值", 34, 700, "$c-accent",
|
||||
family=CJK, line_height=1.3),
|
||||
], gap=0),
|
||||
], gap=40, align="start", padding=[26, 32],
|
||||
stroke={"thickness": {"bottom": 3}, "fill": solid("$c-accent")})
|
||||
|
||||
table = col("对比表", [header] + [
|
||||
table_row(*entry, last=index == len(COMPARISON) - 1)
|
||||
for index, entry in enumerate(COMPARISON)
|
||||
], gap=0, cornerRadius=20, fill=solid("$c-surface"), clipContent=True)
|
||||
|
||||
s = slide("05 对比", gap=56)
|
||||
s["children"] = [
|
||||
page_head("两个概念,别再混着用",
|
||||
"单调性说的是一段,极值说的是一点。"),
|
||||
centered_body("对比区", table),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 06 小结
|
||||
TAKEAWAYS = [
|
||||
"先求导,再找驻点,最后判符号——顺序不能换。",
|
||||
"驻点只是候选,符号真的变了才是极值点。",
|
||||
"不可导的点也要单独拿出来讨论。",
|
||||
]
|
||||
|
||||
HOMEWORK = [
|
||||
("课本 P128", "习题 3.4 第 1、3、5 题"),
|
||||
("补充题", "讨论 f(x) = x·e^(−x) 的单调区间与极值"),
|
||||
("下次课前", "预习凹凸性与拐点(P131–P136)"),
|
||||
]
|
||||
|
||||
|
||||
def summary():
|
||||
left = col("本讲小结", [
|
||||
text(ids, "小结标题", "本讲小结", 40, 700, "$c-ink", family=CJK,
|
||||
line_height=1.2),
|
||||
col("小结条目", [
|
||||
row("小结条", [bullet_dot(),
|
||||
text(ids, "条目文字", line, 28, 400, "$c-ink", family=CJK,
|
||||
line_height=1.55)],
|
||||
gap=20, align="start")
|
||||
for line in TAKEAWAYS
|
||||
], gap=20),
|
||||
], gap=28, height="fill_container", padding=[44, 44], cornerRadius=20,
|
||||
fill=solid("$c-surface"))
|
||||
|
||||
right = col("课后作业", [
|
||||
text(ids, "作业标题", "课后作业", 40, 700, "$c-ink", family=CJK,
|
||||
line_height=1.2),
|
||||
col("作业条目", [
|
||||
col("作业条", [
|
||||
text(ids, "作业出处", source, 26, 600, "$c-accent",
|
||||
family=CJK, line_height=1.3),
|
||||
text(ids, "作业内容", detail, 28, 400, "$c-muted",
|
||||
family=CJK, line_height=1.5),
|
||||
], gap=8)
|
||||
for source, detail in HOMEWORK
|
||||
], gap=24),
|
||||
], gap=28, width=680, height="fill_container", padding=[44, 44],
|
||||
cornerRadius=20, fill=solid("$c-accent-soft"))
|
||||
|
||||
s = slide("06 小结与作业", gap=56)
|
||||
s["children"] = [
|
||||
page_head("带走这三句话就够了"),
|
||||
centered_body("小结区", row("小结两栏", [left, right], gap=48,
|
||||
align="stretch", height=490)),
|
||||
]
|
||||
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(), objectives(), concept(), worked_example(),
|
||||
comparison(), summary()]
|
||||
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 14.25 c-ink on c-surface 16.53
|
||||
# c-muted on c-bg 5.46 c-muted on c-surface 6.33
|
||||
# c-accent on c-bg 5.56 c-accent on c-surface 6.45
|
||||
# c-accent on c-accent-soft 5.11 c-muted on c-accent-soft 5.01 ← 最低
|
||||
# #FFFFFF on c-accent 6.45(序号圆里的白字)
|
||||
# 换主色时先量 c-muted on c-accent-soft:浅色 deck 的下限总是出在
|
||||
# 「弱化文字压在弱化底色上」这一对,不是出在标题上。
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_doc(sys.argv[1], VARS, build(), "课件 deck · 浅色 16:9")
|
||||
207
templates/step0/_generators/tpl_minimal_keynote.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env python3
|
||||
"""minimal-keynote.op — 极简 Keynote deck(8 页 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")
|
||||
396
templates/step0/_generators/tpl_pitch_dark.py
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
#!/usr/bin/env python3
|
||||
"""pitch-deck-dark.op — 深色路演 deck(封面 / 问题 / 方案 / 数据 / 里程碑 / CTA)
|
||||
|
||||
已有的 slide-deck 是暖白商务风,适合汇报;这一套是它的反面:黑底、单强调
|
||||
色、大字号、少而重的信息。路演是「在别人只肯给你八分钟的房间里,把一件
|
||||
事讲到对方想追问」——所以每页只有一个视觉锚点,其余全部让位。
|
||||
|
||||
排版遵循 skills/domains/slides.md 的硬契约:
|
||||
- 每帧固定 1920×1080,绝不 fit_content;内容距边缘 ≥100(这里 120)
|
||||
- 正文 ≥24(取 26-30),标题 ≥40,关键数字 80-200(数据页取 140)
|
||||
- 行高:标题 1.1-1.2,正文 1.4+
|
||||
- 最多 2 个字体族(Noto Sans SC 排中文,Inter 排数字),靠字重分层
|
||||
- 强调色只用于强调;正文一律中性色
|
||||
|
||||
深底上的对比度(WCAG,实测见文件末尾)最低一对是 4.73: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, stroke, text, write_doc
|
||||
|
||||
ids = Ids()
|
||||
|
||||
# 近黑的石墨蓝 + 一个电光蓝。深色 deck 最容易翻车的地方是「深色 = 一堆
|
||||
# 灰」:所以底色只有两级(bg / surface),层级靠字重和强调色拉,不靠再多
|
||||
# 铺几层灰面。
|
||||
VARS = color_vars({
|
||||
"c-bg": "#0B1220",
|
||||
"c-surface": "#18263F",
|
||||
"c-ink": "#F2F6FF",
|
||||
"c-muted": "#93A4C4",
|
||||
"c-accent": "#4D8DFF",
|
||||
"c-accent-soft": "#16233D",
|
||||
"c-border": "#24314D",
|
||||
})
|
||||
|
||||
W, H = 1920, 1080
|
||||
EDGE = 120
|
||||
|
||||
# 中文字族写死:oplib 的默认族按字号/字重挑(<34 且 <600 走 Inter),那条
|
||||
# 规则是给中英混排写的;这套 deck 是纯中文正文,小字号也必须落在有汉字的
|
||||
# 字族上,否则要靠 fallback 兜。
|
||||
CJK = "Noto Sans SC"
|
||||
# 数字、单位符号走 Inter:等宽感更强,KPI 排出来才齐。
|
||||
NUM = "Inter"
|
||||
|
||||
|
||||
def slide(name, *, gap=56, justify="start", fill="$c-bg"):
|
||||
"""一帧幻灯片。固定 1920×1080 —— 投影比例是硬约束,不是建议。"""
|
||||
node = frame(ids, name, width=W, height=H, layout="vertical",
|
||||
padding=[EDGE, EDGE], gap=gap, justifyContent=justify,
|
||||
alignItems="start", fill=solid(fill), clipContent=True)
|
||||
node["children"] = []
|
||||
return node
|
||||
|
||||
|
||||
def col(name, children, *, gap=16, width="fill_container",
|
||||
height="fit_content", align="start", fill=None, **props):
|
||||
"""纵向容器。`fill=None` 才是结构层;一旦有 fill 它就是一张卡面了。"""
|
||||
node = frame(ids, name, width=width, height=height, layout="vertical",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def row(name, children, *, gap=40, align="start", width="fill_container",
|
||||
height="fit_content", fill=None, **props):
|
||||
node = frame(ids, name, width=width, height=height, layout="horizontal",
|
||||
gap=gap, alignItems=align, fill=fill or [], **props)
|
||||
node["children"] = children
|
||||
return node
|
||||
|
||||
|
||||
def eyebrow(label):
|
||||
"""小标签。整套 deck 唯一的胶囊,用来交代「这是什么场合」。"""
|
||||
node = frame(ids, "标签", width="fit_content", height="fit_content",
|
||||
layout="horizontal", padding=[10, 22], gap=0,
|
||||
cornerRadius=999, alignItems="center",
|
||||
justifyContent="center", fill=solid("$c-accent-soft"))
|
||||
node["children"] = [
|
||||
text(ids, "标签文字", label, 26, 600, "$c-accent", family=CJK,
|
||||
width="fit_content", growth="auto", line_height=1.4,
|
||||
spacing=1.0),
|
||||
]
|
||||
return node
|
||||
|
||||
|
||||
def accent_bar(width=160, height=10):
|
||||
return rect(ids, "强调条", width=width, height=height, cornerRadius=999,
|
||||
fill=solid("$c-accent"))
|
||||
|
||||
|
||||
def divider():
|
||||
return rect(ids, "分隔线", width="fill_container", height=2,
|
||||
fill=solid("$c-border"))
|
||||
|
||||
|
||||
def centered_body(name, child):
|
||||
"""把内容块撑到页标题以下的整块空间里再垂直居中。
|
||||
|
||||
页标题每页固定在同一高度(重复)是 deck 读起来整齐的前提;正文块又不
|
||||
该被拉长到失真。两个诉求靠这一层透明容器同时满足。
|
||||
"""
|
||||
return col(name, [child], gap=0, height="fill_container",
|
||||
justifyContent="center")
|
||||
|
||||
|
||||
def page_head(title, subtitle=None, *, size=64):
|
||||
kids = [text(ids, "页标题", title, size, 700, "$c-ink", family=CJK,
|
||||
line_height=1.15)]
|
||||
if subtitle:
|
||||
kids.append(text(ids, "页副标题", subtitle, 30, 400, "$c-muted",
|
||||
family=CJK, line_height=1.45))
|
||||
return col("页头", kids, gap=18)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 01 封面
|
||||
def cover():
|
||||
lede = col("主张", [
|
||||
text(ids, "主标题", "把三天的报价\n压进三分钟", 112, 700, "$c-ink",
|
||||
family=CJK, line_height=1.12),
|
||||
accent_bar(),
|
||||
text(ids, "副标题", "我们在做的,是让每一家工厂当场就能报出价来。",
|
||||
34, 400, "$c-muted", family=CJK, line_height=1.45),
|
||||
], gap=36)
|
||||
|
||||
meta = row("落款", [
|
||||
text(ids, "演讲者", "启明智造 · 创始人 陈知远", 28, 500, "$c-ink",
|
||||
family=CJK, width="fit_content", growth="auto"),
|
||||
text(ids, "场合", "2026.08 · 种子轮路演", 28, 400, "$c-muted",
|
||||
family=CJK, width="fit_content", growth="auto"),
|
||||
], gap=32, align="center", justifyContent="space_between")
|
||||
|
||||
s = slide("01 封面", gap=48, justify="space_between")
|
||||
s["children"] = [eyebrow("种子轮 · 融资路演"), lede, meta]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 02 问题
|
||||
PAINS = [
|
||||
("算不清", "一份报价要问遍四个部门,等两天才知道能不能接。"),
|
||||
("报不准", "凭经验拍的价,做完才发现毛利被工时和损耗吃光了。"),
|
||||
("跟不上", "客户当场要数,我们只能说「回去核一下」——单子就这么走了。"),
|
||||
]
|
||||
|
||||
|
||||
def problem():
|
||||
rows = []
|
||||
for index, (title, desc) in enumerate(PAINS, 1):
|
||||
if rows:
|
||||
rows.append(divider())
|
||||
rows.append(row(f"痛点 {index}", [
|
||||
text(ids, "痛点序号", f"0{index}", 64, 700, "$c-accent",
|
||||
family=NUM, width="fit_content", growth="auto",
|
||||
line_height=1.0),
|
||||
col("痛点文案", [
|
||||
text(ids, "痛点标题", title, 40, 600, "$c-ink", family=CJK,
|
||||
line_height=1.2),
|
||||
text(ids, "痛点说明", desc, 28, 400, "$c-muted", family=CJK,
|
||||
line_height=1.5),
|
||||
], gap=12),
|
||||
], gap=40, align="start"))
|
||||
|
||||
s = slide("02 问题", gap=56)
|
||||
s["children"] = [
|
||||
page_head("报价这一步,卡住了整条链路",
|
||||
"三个问题,每一个都在直接吃掉订单。"),
|
||||
col("痛点列表", rows, gap=0, height="fill_container",
|
||||
justifyContent="space_between"),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 03 方案
|
||||
SELLING_POINTS = [
|
||||
("三分钟出价", "上传图纸,系统直接算出工时、材料与损耗。"),
|
||||
("毛利先看见", "每一档价格旁边就是毛利率,接不接一眼决定。"),
|
||||
("越报越准", "每完成一单回流真实成本,模型自己往回修。"),
|
||||
]
|
||||
|
||||
|
||||
def solution():
|
||||
cards = []
|
||||
for title, desc in SELLING_POINTS:
|
||||
cards.append(col("卖点卡", [
|
||||
rect(ids, "卡首强调条", width=56, height=6, cornerRadius=999,
|
||||
fill=solid("$c-accent")),
|
||||
text(ids, "卖点标题", title, 38, 600, "$c-ink", family=CJK,
|
||||
line_height=1.2),
|
||||
text(ids, "卖点说明", desc, 27, 400, "$c-muted", family=CJK,
|
||||
line_height=1.55),
|
||||
], gap=22, height="fill_container", padding=[48, 40],
|
||||
cornerRadius=20, fill=solid("$c-surface")))
|
||||
|
||||
s = slide("03 方案", gap=56)
|
||||
s["children"] = [
|
||||
page_head("把报价变成一次点击",
|
||||
"工艺库 + 实时成本模型,客户还在电话里,价就出来了。"),
|
||||
centered_body("卖点区", row("卖点网格", cards, gap=36,
|
||||
align="stretch")),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 04 数据
|
||||
METRICS = [
|
||||
("42", "倍", "报价提速", "两天的活儿,三分钟出结果"),
|
||||
("97", "%", "报价准确率", "与实际成本偏差不超过 3%"),
|
||||
("2.3", "亿元", "在跑的订单金额", "来自 46 家制造企业"),
|
||||
]
|
||||
|
||||
# 大数字 lineHeight 1.0 时基线离行盒底部约 0.2em;单位字号小得多,`end`
|
||||
# 对齐会把它压到基线以下。这个补偿把单位的行盒底抬回大数字的基线附近,
|
||||
# 让「42」和「×」看起来站在同一条线上。jian 没有真正的 baseline 对齐
|
||||
# (AlignItems::from_css 把 "baseline" 折成 End),所以只能这样补。
|
||||
VALUE_SIZE = 140
|
||||
UNIT_SIZE = 44
|
||||
UNIT_BASELINE_LIFT = round((VALUE_SIZE - UNIT_SIZE) * 0.2)
|
||||
|
||||
|
||||
def unit_family(unit):
|
||||
"""汉字单位(亿元)排中文族,符号单位(× %)排 Inter 跟数字同源。"""
|
||||
return CJK if any(ord(ch) > 0x2FFF for ch in unit) else NUM
|
||||
|
||||
|
||||
def metric_card(value, unit, label, note):
|
||||
unit_box = frame(ids, "单位", width="fit_content", height="fit_content",
|
||||
layout="vertical", gap=0,
|
||||
padding=[0, 0, UNIT_BASELINE_LIFT, 0], fill=[])
|
||||
unit_box["children"] = [
|
||||
text(ids, "单位文字", unit, UNIT_SIZE, 600, "$c-accent",
|
||||
family=unit_family(unit), width="fit_content", growth="auto",
|
||||
line_height=1.0),
|
||||
]
|
||||
return col("数据卡", [
|
||||
row("数值", [
|
||||
text(ids, "数值文字", value, VALUE_SIZE, 700, "$c-accent",
|
||||
family=NUM, width="fit_content", growth="auto",
|
||||
line_height=1.0),
|
||||
unit_box,
|
||||
], gap=10, align="end", width="fit_content"),
|
||||
text(ids, "指标名", label, 32, 600, "$c-ink", family=CJK,
|
||||
line_height=1.25),
|
||||
text(ids, "指标说明", note, 26, 400, "$c-muted", family=CJK,
|
||||
line_height=1.45),
|
||||
], gap=20, height="fill_container", justifyContent="center",
|
||||
padding=[56, 44], cornerRadius=24, fill=solid("$c-surface"))
|
||||
|
||||
|
||||
def metrics():
|
||||
s = slide("04 数据", gap=56)
|
||||
s["children"] = [
|
||||
page_head("半年里,它已经在跑",
|
||||
"不是路演里的假设,是 46 家工厂每天在用的数。"),
|
||||
centered_body("数据区", row("数据网格",
|
||||
[metric_card(*entry) for entry in METRICS],
|
||||
gap=36, align="stretch")),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# --------------------------------------------------------------- 05 里程碑
|
||||
MILESTONES = [
|
||||
("2026 Q3", "工艺库上线", "覆盖钣金、机加两条主线", True),
|
||||
("2026 Q4", "接入 ERP", "报价直接落成订单", True),
|
||||
("2027 Q1", "开放 API", "让客户在自己系统里报价", False),
|
||||
("2027 Q2", "行业模板", "从制造扩到工程与装修", False),
|
||||
]
|
||||
|
||||
# 时间轴列之间不留 gap:轴线是每列自己那一段拼起来的,列间一旦有间隙,
|
||||
# 轴就断成四截。列内文案靠右 padding 让出呼吸,轴线段照样贯穿。
|
||||
TIMELINE_TEXT_INSET = 56
|
||||
DOT = 28
|
||||
|
||||
|
||||
def milestone_column(date, title, desc, reached):
|
||||
"""一段时间轴。圆点与文案都左对齐,轴线从圆点向右延伸到下一个圆点。"""
|
||||
if reached:
|
||||
dot = rect(ids, "节点圆", width=DOT, height=DOT, cornerRadius=DOT // 2,
|
||||
fill=solid("$c-accent"))
|
||||
else:
|
||||
dot = rect(ids, "节点圆", width=DOT, height=DOT, cornerRadius=DOT // 2,
|
||||
fill=solid("$c-bg"), stroke=stroke("$c-accent", 4))
|
||||
|
||||
axis = row("轴段", [
|
||||
dot,
|
||||
rect(ids, "轴线", width="fill_container", height=4,
|
||||
fill=solid("$c-border")),
|
||||
], gap=0, align="center", height="fit_content")
|
||||
|
||||
# 上下两块文案带右内缩,轴段一律不写 padding —— 三个同类兄弟里只有两个
|
||||
# 可归一化的 padding 值,mixed-sibling-padding 因此不会误判(同一个值
|
||||
# 出现两次,另一个不参与统计)。
|
||||
head = col("节点日期", [
|
||||
text(ids, "日期", date, 28, 600, "$c-accent", family=NUM,
|
||||
line_height=1.2, spacing=0.5),
|
||||
], gap=0, padding=[0, TIMELINE_TEXT_INSET, 0, 0])
|
||||
body = col("节点文案", [
|
||||
text(ids, "节点标题", title, 34, 600, "$c-ink", family=CJK,
|
||||
line_height=1.25),
|
||||
text(ids, "节点说明", desc, 26, 400, "$c-muted", family=CJK,
|
||||
line_height=1.5),
|
||||
], gap=10, padding=[0, TIMELINE_TEXT_INSET, 0, 0])
|
||||
|
||||
return col("里程碑", [head, axis, body], gap=28)
|
||||
|
||||
|
||||
def roadmap():
|
||||
s = slide("05 里程碑", gap=56)
|
||||
s["children"] = [
|
||||
page_head("接下来十二个月",
|
||||
"前两步已经走完,这轮钱花在后两步上。"),
|
||||
centered_body("时间轴区",
|
||||
row("时间轴",
|
||||
[milestone_column(*entry) for entry in MILESTONES],
|
||||
gap=0, align="start")),
|
||||
]
|
||||
return s
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 06 结尾
|
||||
def closing():
|
||||
lede = col("结语", [
|
||||
accent_bar(),
|
||||
text(ids, "结语标题", "如果你也觉得\n这事早该被解决", 88, 700,
|
||||
"$c-ink", family=CJK, line_height=1.15),
|
||||
text(ids, "结语副标题", "我们缺的不是订单,是把它做快的那笔钱。",
|
||||
32, 400, "$c-muted", family=CJK, line_height=1.45),
|
||||
], gap=36)
|
||||
|
||||
# 名字与两行联络方式分成两层,不是排版洁癖:三个平级文本一旦字号 32 /
|
||||
# 30 / 30,sibling-inconsistency 会把那个 32 当成手滑。分层之后 32 是
|
||||
# 标题、30 是并列的两行,各自内部一致。
|
||||
contact = col("联系方式", [
|
||||
text(ids, "联系人", "陈知远 · 创始人", 32, 600, "$c-ink", family=CJK,
|
||||
line_height=1.3),
|
||||
col("联络方式", [
|
||||
text(ids, "邮箱", "zhiyuan@qimingzz.com", 30, 500, "$c-accent",
|
||||
family=NUM, line_height=1.4),
|
||||
text(ids, "电话", "+86 138 0000 0000", 30, 400, "$c-muted",
|
||||
family=NUM, line_height=1.4),
|
||||
], gap=10, width="fit_content"),
|
||||
], gap=18, width="fit_content")
|
||||
|
||||
# 二维码占位用 frame 而不是 rectangle:frame 会递归渲染子节点,里面那行
|
||||
# 提示才显示得出来;frame 同时是合法的图片拖放目标,把二维码图片拖上去
|
||||
# 就直接替换掉这个占位。
|
||||
qr = frame(ids, "二维码占位", width=200, height=200, layout="vertical",
|
||||
gap=0, alignItems="center", justifyContent="center",
|
||||
cornerRadius=16, fill=solid("$c-accent-soft"),
|
||||
stroke=stroke("$c-accent", 2))
|
||||
qr["children"] = [
|
||||
text(ids, "二维码提示", "扫码看 Demo", 26, 600, "$c-accent",
|
||||
family=CJK, width="fit_content", growth="auto",
|
||||
line_height=1.3),
|
||||
]
|
||||
|
||||
card = row("联系卡", [contact, qr], gap=48, align="center",
|
||||
justifyContent="space_between", padding=[48, 56],
|
||||
cornerRadius=24, fill=solid("$c-surface"))
|
||||
|
||||
s = slide("06 结尾", gap=56, justify="space_between")
|
||||
s["children"] = [lede, card]
|
||||
return s
|
||||
|
||||
|
||||
# 顶层 frame 必须显式写 x/y:缺省时每帧都落在原点,六页会叠成一页,另外
|
||||
# 五页藏在下面(2026-08-02 实录)。横向 gap 与纵向 gap 的取值同 tpl_slides:
|
||||
# 纵向多留 240,是给画布上固定在帧顶部的帧名标签让位——标签是屏幕空间的,
|
||||
# 不随缩放变小,deck 缩到能整屏看时 120 文档像素只剩十几个屏幕像素。
|
||||
BOARD_GAP_X = 120
|
||||
BOARD_GAP_Y = BOARD_GAP_X + 240
|
||||
BOARDS_PER_ROW = 3
|
||||
|
||||
|
||||
def build():
|
||||
boards = [cover(), problem(), solution(), metrics(), roadmap(), 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 17.30 c-ink on c-surface 13.98
|
||||
# c-muted on c-bg 7.44 c-muted on c-surface 6.01
|
||||
# c-accent on c-bg 5.86 c-accent on c-surface 4.73 ← 最低一对
|
||||
# c-accent on c-accent-soft 4.90
|
||||
# 数据页的 140px 大数字用的是 c-accent on c-surface = 4.73:1。换强调色时
|
||||
# 先拿这一对量一遍:深底上亮色越纯,比值掉得越快。
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_doc(sys.argv[1], VARS, build(), "路演 deck · 深色 16:9")
|
||||
BIN
templates/step0/previews/gradient-tech-01.png
Normal file
|
After Width: | Height: | Size: 493 KiB |
BIN
templates/step0/previews/gradient-tech-02.png
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
templates/step0/previews/gradient-tech-03.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
templates/step0/previews/gradient-tech-04.png
Normal file
|
After Width: | Height: | Size: 519 KiB |
BIN
templates/step0/previews/gradient-tech-05.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
BIN
templates/step0/previews/gradient-tech-06.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
BIN
templates/step0/previews/gradient-tech-overview.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
templates/step0/previews/knowledge-card-square.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
templates/step0/previews/knowledge-card-vertical.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
templates/step0/previews/lecture-deck-light-01.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
templates/step0/previews/lecture-deck-light-02.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
templates/step0/previews/lecture-deck-light-03.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
templates/step0/previews/lecture-deck-light-04.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
templates/step0/previews/lecture-deck-light-05.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
templates/step0/previews/lecture-deck-light-06.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
templates/step0/previews/lecture-deck-light-overview.png
Normal file
|
After Width: | Height: | Size: 628 KiB |
BIN
templates/step0/previews/minimal-keynote-01.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
templates/step0/previews/minimal-keynote-02.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
templates/step0/previews/minimal-keynote-03.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
templates/step0/previews/minimal-keynote-04.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
templates/step0/previews/minimal-keynote-05.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
templates/step0/previews/minimal-keynote-06.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
templates/step0/previews/minimal-keynote-07.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
templates/step0/previews/minimal-keynote-08.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
templates/step0/previews/minimal-keynote-overview.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
templates/step0/previews/pitch-deck-dark-01.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
templates/step0/previews/pitch-deck-dark-02.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
templates/step0/previews/pitch-deck-dark-03.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
templates/step0/previews/pitch-deck-dark-04.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
templates/step0/previews/pitch-deck-dark-05.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
templates/step0/previews/pitch-deck-dark-06.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
templates/step0/previews/pitch-deck-dark-overview.png
Normal file
|
After Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 87 KiB |
BIN
templates/step0/previews/slide-deck-overview.png
Normal file
|
After Width: | Height: | Size: 414 KiB |