diff --git a/.gitignore b/.gitignore index b9dbf9d09..20c51e519 100644 --- a/.gitignore +++ b/.gitignore @@ -58,5 +58,7 @@ Cargo.lock.merge-conflict **/locator-signing-key*.json tools/__pycache__/ +__pycache__/ +*.pyc openpencil-docs output diff --git a/crates/op-editor-core/assets/scene_templates.toml b/crates/op-editor-core/assets/scene_templates.toml index c3f3b3dae..a83e0c438 100644 --- a/crates/op-editor-core/assets/scene_templates.toml +++ b/crates/op-editor-core/assets/scene_templates.toml @@ -33,6 +33,18 @@ frames = 5 frame_width = 1080 frame_height = 1440 +[[template]] +id = "slide-deck" +scene = "slides" +title_key = "sceneTemplate.item.slideDeck.title" +title_fallback = "演示文稿 · 六页" +summary_key = "sceneTemplate.item.slideDeck.summary" +summary_fallback = "封面、目录、要点、数据、图表和结尾,16:9 投影比例,替换文案即可上台。" +tags = ["PPT", "演示", "汇报", "幻灯片", "slides", "deck", "presentation"] +frames = 6 +frame_width = 1920 +frame_height = 1080 + [[template]] id = "before-after" scene = "comparison" diff --git a/crates/op-editor-core/assets/scene_templates/slide-deck.op b/crates/op-editor-core/assets/scene_templates/slide-deck.op new file mode 100644 index 000000000..3e12b333c --- /dev/null +++ b/crates/op-editor-core/assets/scene_templates/slide-deck.op @@ -0,0 +1,1658 @@ +{ + "version": "1.0.0", + "name": "演示文稿 · 16:9 模板", + "variables": { + "c-bg": { + "type": "color", + "value": "#FFFFFF" + }, + "c-surface": { + "type": "color", + "value": "#F4F6F9" + }, + "c-ink": { + "type": "color", + "value": "#0B1220" + }, + "c-muted": { + "type": "color", + "value": "#5A6B85" + }, + "c-accent": { + "type": "color", + "value": "#2F5BEA" + }, + "c-accent-soft": { + "type": "color", + "value": "#E4EAFD" + }, + "c-border": { + "type": "color", + "value": "#DCE2EC" + } + }, + "children": [ + { + "type": "frame", + "id": "f1", + "name": "01 封面", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 40, + "justifyContent": "center", + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "clipContent": true, + "children": [ + { + "type": "frame", + "id": "f2", + "name": "封面文案", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 32, + "fill": [], + "children": [ + { + "type": "frame", + "id": "f3", + "name": "标签", + "width": "fit_content", + "height": "fit_content", + "layout": "horizontal", + "padding": [ + 8, + 18 + ], + "cornerRadius": 999, + "fill": [ + { + "type": "solid", + "color": "$c-accent-soft" + } + ], + "children": [ + { + "type": "text", + "id": "t4", + "name": "标签文字", + "content": "2026 · 季度汇报", + "fontFamily": "Noto Sans SC", + "fontSize": 24, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.6, + "letterSpacing": 1.2, + "textGrowth": "fixed-width", + "width": "fit_content" + } + ] + }, + { + "type": "text", + "id": "t5", + "name": "主标题", + "content": "把复杂的事,讲成一句话", + "fontFamily": "Noto Sans SC", + "fontSize": 104, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t6", + "name": "副标题", + "content": "副标题写清楚这次要让听众记住的那个结论。", + "fontFamily": "Noto Sans SC", + "fontSize": 34, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "rectangle", + "id": "r7", + "name": "强调条", + "width": 120, + "height": 8, + "cornerRadius": 999, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ] + } + ] + }, + { + "type": "frame", + "id": "f8", + "name": "落款", + "width": "fill_container", + "height": "fit_content", + "layout": "horizontal", + "gap": 32, + "alignItems": "center", + "fill": [], + "children": [ + { + "type": "text", + "id": "t9", + "name": "演讲者", + "content": "演讲者姓名 · 团队", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": 500, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.6, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t10", + "name": "日期", + "content": "2026.08", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.6, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + } + ] + } + ] + }, + { + "type": "frame", + "id": "f11", + "name": "02 目录", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 48, + "justifyContent": "start", + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "clipContent": true, + "children": [ + { + "type": "text", + "id": "t12", + "name": "页标题", + "content": "目录", + "fontFamily": "Noto Sans SC", + "fontSize": 64, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "frame", + "id": "f13", + "name": "目录网格", + "width": "fill_container", + "height": "fit_content", + "layout": "horizontal", + "gap": 32, + "alignItems": "start", + "fill": [], + "children": [ + { + "type": "frame", + "id": "f14", + "name": "目录项", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 14, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 36, + 32 + ], + "cornerRadius": 20, + "children": [ + { + "type": "text", + "id": "t15", + "name": "序号", + "content": "01", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t16", + "name": "条目标题", + "content": "背景与问题", + "fontFamily": "Noto Sans SC", + "fontSize": 34, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t17", + "name": "条目说明", + "content": "我们面对的现状是什么", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.5, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f18", + "name": "目录项", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 14, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 36, + 32 + ], + "cornerRadius": 20, + "children": [ + { + "type": "text", + "id": "t19", + "name": "序号", + "content": "02", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t20", + "name": "条目标题", + "content": "解决方案", + "fontFamily": "Noto Sans SC", + "fontSize": 34, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t21", + "name": "条目说明", + "content": "做了哪三件关键的事", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.5, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f22", + "name": "目录项", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 14, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 36, + 32 + ], + "cornerRadius": 20, + "children": [ + { + "type": "text", + "id": "t23", + "name": "序号", + "content": "03", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t24", + "name": "条目标题", + "content": "结果与数据", + "fontFamily": "Noto Sans SC", + "fontSize": 34, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t25", + "name": "条目说明", + "content": "带来了多少可衡量的变化", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.5, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f26", + "name": "目录项", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 14, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 36, + 32 + ], + "cornerRadius": 20, + "children": [ + { + "type": "text", + "id": "t27", + "name": "序号", + "content": "04", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t28", + "name": "条目标题", + "content": "下一步", + "fontFamily": "Noto Sans SC", + "fontSize": 34, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t29", + "name": "条目说明", + "content": "接下来三个月的计划", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.5, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f30", + "name": "03 要点", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 48, + "justifyContent": "start", + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "clipContent": true, + "children": [ + { + "type": "frame", + "id": "f31", + "name": "页头", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 16, + "fill": [], + "children": [ + { + "type": "text", + "id": "t32", + "name": "页标题", + "content": "我们做了三件事", + "fontFamily": "Noto Sans SC", + "fontSize": 64, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t33", + "name": "页副标题", + "content": "每一件都能单独讲清楚,也都能被验证。", + "fontFamily": "Inter", + "fontSize": 30, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f34", + "name": "要点网格", + "width": "fill_container", + "height": "fit_content", + "layout": "horizontal", + "gap": 36, + "alignItems": "start", + "fill": [], + "children": [ + { + "type": "frame", + "id": "f35", + "name": "要点", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 20, + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "padding": [ + 40, + 36 + ], + "cornerRadius": 20, + "stroke": { + "thickness": 2, + "fill": [ + { + "type": "solid", + "color": "$c-border" + } + ] + }, + "children": [ + { + "type": "text", + "id": "t36", + "name": "要点序号", + "content": "01", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t37", + "name": "要点标题", + "content": "更快", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.2, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t38", + "name": "要点说明", + "content": "把等待时间从 8 秒压到 1.2 秒,用户不再中途离开。", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f39", + "name": "要点", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 20, + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "padding": [ + 40, + 36 + ], + "cornerRadius": 20, + "stroke": { + "thickness": 2, + "fill": [ + { + "type": "solid", + "color": "$c-border" + } + ] + }, + "children": [ + { + "type": "text", + "id": "t40", + "name": "要点序号", + "content": "02", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t41", + "name": "要点标题", + "content": "更稳", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.2, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t42", + "name": "要点说明", + "content": "关键路径的失败率下降到千分之一以内。", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f43", + "name": "要点", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 20, + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "padding": [ + 40, + 36 + ], + "cornerRadius": 20, + "stroke": { + "thickness": 2, + "fill": [ + { + "type": "solid", + "color": "$c-border" + } + ] + }, + "children": [ + { + "type": "text", + "id": "t44", + "name": "要点序号", + "content": "03", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.1, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fit_content" + }, + { + "type": "text", + "id": "t45", + "name": "要点标题", + "content": "更省", + "fontFamily": "Noto Sans SC", + "fontSize": 40, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.2, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t46", + "name": "要点说明", + "content": "同样的任务量,只用原来一半的算力预算。", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f47", + "name": "04 数据", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 48, + "justifyContent": "start", + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "clipContent": true, + "children": [ + { + "type": "text", + "id": "t48", + "name": "页标题", + "content": "结果是可衡量的", + "fontFamily": "Noto Sans SC", + "fontSize": 64, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "frame", + "id": "f49", + "name": "数据网格", + "width": "fill_container", + "height": "fill_container", + "layout": "horizontal", + "gap": 36, + "alignItems": "stretch", + "fill": [], + "children": [ + { + "type": "frame", + "id": "f50", + "name": "数据卡", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 18, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 56, + 44 + ], + "cornerRadius": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "t51", + "name": "数值", + "content": "1.2s", + "fontFamily": "Noto Sans SC", + "fontSize": 140, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.05, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t52", + "name": "指标名", + "content": "首屏加载", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t53", + "name": "变化", + "content": "较上季度 -85%", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f54", + "name": "数据卡", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 18, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 56, + 44 + ], + "cornerRadius": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "t55", + "name": "数值", + "content": "99.9%", + "fontFamily": "Noto Sans SC", + "fontSize": 140, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.05, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t56", + "name": "指标名", + "content": "关键路径成功率", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t57", + "name": "变化", + "content": "连续 90 天达标", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f58", + "name": "数据卡", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 18, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "padding": [ + 56, + 44 + ], + "cornerRadius": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "t59", + "name": "数值", + "content": "2.1x", + "fontFamily": "Noto Sans SC", + "fontSize": 140, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ], + "lineHeight": 1.05, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t60", + "name": "指标名", + "content": "人均处理量", + "fontFamily": "Noto Sans SC", + "fontSize": 32, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t61", + "name": "变化", + "content": "同等人力下", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f62", + "name": "05 图表", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 48, + "justifyContent": "start", + "fill": [ + { + "type": "solid", + "color": "$c-bg" + } + ], + "clipContent": true, + "children": [ + { + "type": "frame", + "id": "f63", + "name": "页头", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 16, + "fill": [], + "children": [ + { + "type": "text", + "id": "t64", + "name": "页标题", + "content": "趋势说明结论", + "fontFamily": "Noto Sans SC", + "fontSize": 64, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t65", + "name": "页副标题", + "content": "把图表要说明的那句话写在这里,别让听众自己找。", + "fontFamily": "Inter", + "fontSize": 30, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f66", + "name": "图表区", + "width": "fill_container", + "height": "fill_container", + "layout": "horizontal", + "gap": 48, + "alignItems": "stretch", + "fill": [], + "children": [ + { + "type": "frame", + "id": "f67", + "name": "图表占位", + "width": "fill_container", + "height": "fill_container", + "layout": "horizontal", + "gap": 28, + "alignItems": "end", + "padding": [ + 40, + 40 + ], + "cornerRadius": 24, + "fill": [ + { + "type": "solid", + "color": "$c-surface" + } + ], + "children": [ + { + "type": "rectangle", + "id": "r68", + "name": "柱 1", + "width": "fill_container", + "height": 180, + "cornerRadius": 12, + "fill": [ + { + "type": "solid", + "color": "$c-accent-soft" + } + ] + }, + { + "type": "rectangle", + "id": "r69", + "name": "柱 2", + "width": "fill_container", + "height": 250, + "cornerRadius": 12, + "fill": [ + { + "type": "solid", + "color": "$c-accent-soft" + } + ] + }, + { + "type": "rectangle", + "id": "r70", + "name": "柱 3", + "width": "fill_container", + "height": 220, + "cornerRadius": 12, + "fill": [ + { + "type": "solid", + "color": "$c-accent-soft" + } + ] + }, + { + "type": "rectangle", + "id": "r71", + "name": "柱 4", + "width": "fill_container", + "height": 330, + "cornerRadius": 12, + "fill": [ + { + "type": "solid", + "color": "$c-accent-soft" + } + ] + }, + { + "type": "rectangle", + "id": "r72", + "name": "柱 5", + "width": "fill_container", + "height": 420, + "cornerRadius": 12, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ] + } + ] + }, + { + "type": "frame", + "id": "f73", + "name": "图表说明", + "width": 520, + "height": "fit_content", + "layout": "vertical", + "gap": 24, + "fill": [], + "children": [ + { + "type": "frame", + "id": "f74", + "name": "说明项", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 8, + "fill": [], + "children": [ + { + "type": "text", + "id": "t75", + "name": "说明标题", + "content": "持续增长", + "fontFamily": "Noto Sans SC", + "fontSize": 30, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t76", + "name": "说明正文", + "content": "连续五个周期上升,没有出现回落。", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f77", + "name": "说明项", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 8, + "fill": [], + "children": [ + { + "type": "text", + "id": "t78", + "name": "说明标题", + "content": "拐点在第四期", + "fontFamily": "Noto Sans SC", + "fontSize": 30, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t79", + "name": "说明正文", + "content": "投入的三项改动都在这一期生效。", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "f80", + "name": "说明项", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 8, + "fill": [], + "children": [ + { + "type": "text", + "id": "t81", + "name": "说明标题", + "content": "可以外推", + "fontFamily": "Noto Sans SC", + "fontSize": 30, + "fontWeight": 600, + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "lineHeight": 1.25, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t82", + "name": "说明正文", + "content": "按当前斜率,下季度可达成目标线。", + "fontFamily": "Inter", + "fontSize": 26, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "$c-muted" + } + ], + "lineHeight": 1.55, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f83", + "name": "06 结尾", + "width": 1920, + "height": 1080, + "layout": "vertical", + "padding": [ + 120, + 120 + ], + "gap": 40, + "justifyContent": "center", + "fill": [ + { + "type": "solid", + "color": "$c-ink" + } + ], + "clipContent": true, + "children": [ + { + "type": "frame", + "id": "f84", + "name": "结尾文案", + "width": "fill_container", + "height": "fit_content", + "layout": "vertical", + "gap": 32, + "fill": [], + "children": [ + { + "type": "rectangle", + "id": "r85", + "name": "强调条", + "width": 120, + "height": 8, + "cornerRadius": 999, + "fill": [ + { + "type": "solid", + "color": "$c-accent" + } + ] + }, + { + "type": "text", + "id": "t86", + "name": "结语", + "content": "谢谢,欢迎提问。", + "fontFamily": "Noto Sans SC", + "fontSize": 96, + "fontWeight": 700, + "fill": [ + { + "type": "solid", + "color": "#FFFFFF" + } + ], + "lineHeight": 1.15, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "t87", + "name": "联系方式", + "content": "name@example.com · 团队主页", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": 400, + "fill": [ + { + "type": "solid", + "color": "#9FB0CC" + } + ], + "lineHeight": 1.4, + "letterSpacing": 0, + "textGrowth": "fixed-width", + "width": "fill_container" + } + ] + } + ] + } + ] +} diff --git a/crates/op-editor-core/src/scene_template_catalog.rs b/crates/op-editor-core/src/scene_template_catalog.rs index c9621d477..216a606b9 100644 --- a/crates/op-editor-core/src/scene_template_catalog.rs +++ b/crates/op-editor-core/src/scene_template_catalog.rs @@ -26,6 +26,7 @@ const SCREENSHOT_TUTORIAL_OP: &str = include_str!("../assets/scene_templates/screenshot-tutorial.op"); 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"); /// Return the embedded document JSON for a template id. /// @@ -38,6 +39,7 @@ pub fn scene_template_document(template_id: &str) -> Option<&'static str> { "screenshot-tutorial" => Some(SCREENSHOT_TUTORIAL_OP), "knowledge-carousel" => Some(KNOWLEDGE_CAROUSEL_OP), "before-after" => Some(BEFORE_AFTER_OP), + "slide-deck" => Some(SLIDE_DECK_OP), _ => None, } } diff --git a/crates/op-editor-ui/assets/scene_template_previews/slide-deck.jpg b/crates/op-editor-ui/assets/scene_template_previews/slide-deck.jpg new file mode 100644 index 000000000..9168447b6 Binary files /dev/null and b/crates/op-editor-ui/assets/scene_template_previews/slide-deck.jpg differ diff --git a/templates/step0/_generators/previews.sh b/templates/step0/_generators/previews.sh index bcfb92cd1..67385a023 100644 --- a/templates/step0/_generators/previews.sh +++ b/templates/step0/_generators/previews.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# 渲染三套模板的预览图:每帧一张(scale 2)+ 整页拼合总览(scale 1) +# 渲染场景模板的预览图:每帧一张(scale 2)+ 整页拼合总览(scale 1) +# 卡片缩略图由 scene_preview_cards.py 从这些渲染产物再烤一层。 set -euo pipefail R=/Users/fini/workspace/openpencil BIN=$R/target/release/openpencil-desktop @@ -7,7 +8,7 @@ OUT=$R/templates/step0/previews TMP=$(mktemp -d) mkdir -p "$OUT" -for t in screenshot-tutorial knowledge-carousel before-after; do +for t in screenshot-tutorial knowledge-carousel before-after slide-deck; 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. diff --git a/templates/step0/_generators/scene_preview_cards.py b/templates/step0/_generators/scene_preview_cards.py index 45f5132cd..f1620ddff 100644 --- a/templates/step0/_generators/scene_preview_cards.py +++ b/templates/step0/_generators/scene_preview_cards.py @@ -37,12 +37,20 @@ INSET = 12 # (card id, source preview). The overview render is preferred wherever a # template has several frames: what makes a multi-page template legible at # thumbnail size is seeing that it HAS pages, not reading one of them. +# (card id, source). A string names one render; a list is tiled into a grid +# whose column count is chosen to land near the card's own aspect — a 16:9 +# deck laid out 1x6 is a 10:1 strip that shrinks each slide to ~100px inside +# the card, which reads as noise rather than as slides. CARDS = [ ("screenshot-tutorial", "screenshot-tutorial-overview.png"), ("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)]), ] +# Gap between tiles, in source pixels — scaled down with everything else. +TILE_GAP = 48 + def background_colour(image: Image.Image) -> tuple[int, int, int]: """The document's own backdrop, sampled from the render's corners. @@ -64,8 +72,45 @@ def background_colour(image: Image.Image) -> tuple[int, int, int]: return (245, 245, 245) -def bake(source: pathlib.Path) -> Image.Image: - image = Image.open(source) +def grid_columns(tile_aspect: float, count: int) -> int: + """Column count whose tiled aspect sits closest to the card's. + + Chosen rather than fixed so a 16:9 deck tiles 3x2 while a 3:4 carousel + tiles wide — the goal is filling the card, not a particular shape. + """ + card_aspect = CARD_W / CARD_H + best, best_error = count, float("inf") + for columns in range(1, count + 1): + rows = -(-count // columns) + aspect = (columns * tile_aspect) / rows + error = abs(aspect - card_aspect) + if error < best_error: + best, best_error = columns, error + return best + + +def tile(sources: list[pathlib.Path]) -> Image.Image: + images = [Image.open(path).convert("RGB") for path in sources] + width = max(i.width for i in images) + height = max(i.height for i in images) + columns = grid_columns(width / height, len(images)) + rows = -(-len(images) // columns) + canvas = Image.new( + "RGB", + ( + columns * width + (columns - 1) * TILE_GAP, + rows * height + (rows - 1) * TILE_GAP, + ), + background_colour(images[0]), + ) + for index, image in enumerate(images): + column, row = index % columns, index // columns + canvas.paste(image, (column * (width + TILE_GAP), row * (height + TILE_GAP))) + return canvas + + +def bake(source: pathlib.Path | list[pathlib.Path]) -> Image.Image: + image = tile(source) if isinstance(source, list) else Image.open(source) canvas = Image.new("RGB", (CARD_W, CARD_H), background_colour(image)) fitted = image.convert("RGB") fitted.thumbnail((CARD_W - 2 * INSET, CARD_H - 2 * INSET), Image.LANCZOS) @@ -86,11 +131,15 @@ def main() -> int: failed = False for card_id, source_name in CARDS: - source = SRC / source_name - if not source.exists(): - print(f"missing source: {source}", file=sys.stderr) + names = source_name if isinstance(source_name, list) else [source_name] + sources = [SRC / name for name in names] + missing = [path for path in sources if not path.exists()] + if missing: + for path in missing: + print(f"missing source: {path}", file=sys.stderr) failed = True continue + source = sources if isinstance(source_name, list) else sources[0] target = DST / f"{card_id}.jpg" if args.check: status = "ok" if target.exists() else "MISSING" diff --git a/templates/step0/_generators/tpl_slides.py b/templates/step0/_generators/tpl_slides.py new file mode 100644 index 000000000..4fb50b33e --- /dev/null +++ b/templates/step0/_generators/tpl_slides.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""slide-deck.op — 16:9 演示文稿模板(封面 + 目录 + 要点 + 数据 + 图表 + 结尾) + +排版遵循 skills/domains/slides.md 的硬契约: + - 每帧 1920×1080,内容距边缘 ≥100px + - 正文 ≥24(取 28-32),标题 ≥40,关键数字 80-200 + - 行高:标题 1.1-1.2,正文放宽 + - 最多 2 个字体族,靠字重而非字号数量做层级 + - 2-3 个主色 + 中性色,强调色只用于强调 +""" + +import sys + +from oplib import (Ids, color_vars, frame, rect, solid, stroke, text, + write_doc) + +ids = Ids() + +VARS = color_vars({ + "c-bg": "#FFFFFF", + "c-surface": "#F4F6F9", + "c-ink": "#0B1220", + "c-muted": "#5A6B85", + "c-accent": "#2F5BEA", + "c-accent-soft": "#E4EAFD", + "c-border": "#DCE2EC", +}) + +# 幻灯片尺寸与安全区。EDGE 同时是每帧的 padding —— 契约要求 ≥100。 +W, H = 1920, 1080 +EDGE = 120 + + +def slide(name, *, fill="$c-bg", gap=48, justify="start"): + """一帧幻灯片。固定 1920×1080,绝不 fit_content:投影比例是硬约束。""" + node = frame(ids, name, width=W, height=H, layout="vertical", + padding=[EDGE, EDGE], gap=gap, justifyContent=justify, + fill=solid(fill), clipContent=True) + node["children"] = [] + return node + + +def row(name, *, gap=40, align="start", width="fill_container", + height="fit_content", fill=None, **props): + # `fill=None` means a transparent structural wrapper; an explicit fill is + # what makes a row a painted surface (card, panel). + node = frame(ids, name, width=width, height=height, layout="horizontal", + gap=gap, alignItems=align, fill=fill or [], **props) + node["children"] = [] + return node + + +def col(name, *, gap=16, width="fill_container", height="fit_content", + fill=None, **props): + node = frame(ids, name, width=width, height=height, layout="vertical", + gap=gap, fill=fill or [], **props) + node["children"] = [] + return node + + +def eyebrow(label): + """小标签。ALL CAPS 只在这种小标签上使用(契约允许的唯一例外)。""" + node = frame(ids, "标签", width="fit_content", height="fit_content", + layout="horizontal", padding=[8, 18], cornerRadius=999, + fill=solid("$c-accent-soft")) + node["children"] = [ + text(ids, "标签文字", label, 24, 600, "$c-accent", + width="fit_content", spacing=1.2), + ] + return node + + +def accent_bar(width=120, height=8): + return rect(ids, "强调条", width=width, height=height, cornerRadius=999, + fill=solid("$c-accent")) + + +def cover(): + s = slide("01 封面", justify="center", gap=40) + body = col("封面文案", gap=32) + body["children"] = [ + eyebrow("2026 · 季度汇报"), + text(ids, "主标题", "把复杂的事,讲成一句话", 104, 700, "$c-ink", + line_height=1.15), + text(ids, "副标题", "副标题写清楚这次要让听众记住的那个结论。", 34, 400, + "$c-muted", line_height=1.4), + accent_bar(), + ] + meta = row("落款", gap=32, align="center") + meta["children"] = [ + text(ids, "演讲者", "演讲者姓名 · 团队", 28, 500, "$c-ink", + width="fit_content"), + text(ids, "日期", "2026.08", 28, 400, "$c-muted", width="fit_content"), + ] + s["children"] = [body, meta] + return s + + +def agenda(): + entries = [ + ("01", "背景与问题", "我们面对的现状是什么"), + ("02", "解决方案", "做了哪三件关键的事"), + ("03", "结果与数据", "带来了多少可衡量的变化"), + ("04", "下一步", "接下来三个月的计划"), + ] + s = slide("02 目录") + s["children"].append(text(ids, "页标题", "目录", 64, 700, "$c-ink", + line_height=1.15)) + grid = row("目录网格", gap=32) + for no, title, desc in entries: + item = col("目录项", gap=14, padding=[36, 32], cornerRadius=20, + fill=solid("$c-surface"), height="fill_container") + item["children"] = [ + text(ids, "序号", no, 40, 700, "$c-accent", width="fit_content", + line_height=1.1), + text(ids, "条目标题", title, 34, 600, "$c-ink", line_height=1.25), + text(ids, "条目说明", desc, 26, 400, "$c-muted", line_height=1.5), + ] + grid["children"].append(item) + s["children"].append(grid) + return s + + +def points(): + entries = [ + ("更快", "把等待时间从 8 秒压到 1.2 秒,用户不再中途离开。"), + ("更稳", "关键路径的失败率下降到千分之一以内。"), + ("更省", "同样的任务量,只用原来一半的算力预算。"), + ] + s = slide("03 要点") + head = col("页头", gap=16) + head["children"] = [ + text(ids, "页标题", "我们做了三件事", 64, 700, "$c-ink", + line_height=1.15), + text(ids, "页副标题", "每一件都能单独讲清楚,也都能被验证。", 30, 400, + "$c-muted", line_height=1.4), + ] + grid = row("要点网格", gap=36) + for index, (title, desc) in enumerate(entries, 1): + item = col("要点", gap=20, padding=[40, 36], cornerRadius=20, + height="fill_container", fill=solid("$c-bg"), + stroke=stroke("$c-border", 2)) + item["children"] = [ + text(ids, "要点序号", f"0{index}", 32, 700, "$c-accent", + width="fit_content", line_height=1.1), + text(ids, "要点标题", title, 40, 600, "$c-ink", line_height=1.2), + text(ids, "要点说明", desc, 28, 400, "$c-muted", line_height=1.55), + ] + grid["children"].append(item) + s["children"] = [head, grid] + return s + + +def metrics(): + entries = [ + ("1.2s", "首屏加载", "较上季度 -85%"), + ("99.9%", "关键路径成功率", "连续 90 天达标"), + ("2.1x", "人均处理量", "同等人力下"), + ] + s = slide("04 数据") + s["children"].append(text(ids, "页标题", "结果是可衡量的", 64, 700, + "$c-ink", line_height=1.15)) + grid = row("数据网格", gap=36, height="fill_container", align="stretch") + for value, label, delta in entries: + item = col("数据卡", gap=18, padding=[56, 44], cornerRadius=24, + height="fill_container", justifyContent="center", + fill=solid("$c-surface")) + item["children"] = [ + # 关键数字:契约允许 80-200,这里取 140 让它成为唯一的视觉锚点。 + text(ids, "数值", value, 140, 700, "$c-accent", line_height=1.05), + text(ids, "指标名", label, 32, 600, "$c-ink", line_height=1.25), + text(ids, "变化", delta, 26, 400, "$c-muted", line_height=1.4), + ] + grid["children"].append(item) + s["children"].append(grid) + return s + + +def chart(): + s = slide("05 图表") + head = col("页头", gap=16) + head["children"] = [ + text(ids, "页标题", "趋势说明结论", 64, 700, "$c-ink", + line_height=1.15), + text(ids, "页副标题", "把图表要说明的那句话写在这里,别让听众自己找。", + 30, 400, "$c-muted", line_height=1.4), + ] + body = row("图表区", gap=48, height="fill_container", align="stretch") + + # 图表占位:一组等宽柱子。替换成真实数据时改高度即可,不必重建结构。 + plot = frame(ids, "图表占位", width="fill_container", + height="fill_container", layout="horizontal", gap=28, + alignItems="end", padding=[40, 40], cornerRadius=24, + fill=solid("$c-surface")) + plot["children"] = [ + rect(ids, f"柱 {i + 1}", width="fill_container", height=height, + cornerRadius=12, + fill=solid("$c-accent" if i == 4 else "$c-accent-soft")) + for i, height in enumerate([180, 250, 220, 330, 420]) + ] + + notes = col("图表说明", gap=24, width=520) + for title, desc in [ + ("持续增长", "连续五个周期上升,没有出现回落。"), + ("拐点在第四期", "投入的三项改动都在这一期生效。"), + ("可以外推", "按当前斜率,下季度可达成目标线。"), + ]: + item = col("说明项", gap=8) + item["children"] = [ + text(ids, "说明标题", title, 30, 600, "$c-ink", line_height=1.25), + text(ids, "说明正文", desc, 26, 400, "$c-muted", line_height=1.55), + ] + notes["children"].append(item) + + body["children"] = [plot, notes] + s["children"] = [head, body] + return s + + +def closing(): + s = slide("06 结尾", fill="$c-ink", justify="center", gap=40) + body = col("结尾文案", gap=32) + body["children"] = [ + accent_bar(), + text(ids, "结语", "谢谢,欢迎提问。", 96, 700, "#FFFFFF", + line_height=1.15), + text(ids, "联系方式", "name@example.com · 团队主页", 32, 400, + "#9FB0CC", line_height=1.4), + ] + s["children"] = [body] + return s + + +def build(): + return [cover(), agenda(), points(), metrics(), chart(), closing()] + + +if __name__ == "__main__": + dst = sys.argv[1] + write_doc(dst, VARS, build(), "演示文稿 · 16:9 模板") diff --git a/templates/step0/previews/slide-deck-01.png b/templates/step0/previews/slide-deck-01.png new file mode 100644 index 000000000..84bdb7da8 Binary files /dev/null and b/templates/step0/previews/slide-deck-01.png differ diff --git a/templates/step0/previews/slide-deck-02.png b/templates/step0/previews/slide-deck-02.png new file mode 100644 index 000000000..27eda1761 Binary files /dev/null and b/templates/step0/previews/slide-deck-02.png differ diff --git a/templates/step0/previews/slide-deck-03.png b/templates/step0/previews/slide-deck-03.png new file mode 100644 index 000000000..47c12b6bb Binary files /dev/null and b/templates/step0/previews/slide-deck-03.png differ diff --git a/templates/step0/previews/slide-deck-04.png b/templates/step0/previews/slide-deck-04.png new file mode 100644 index 000000000..bd6b82260 Binary files /dev/null and b/templates/step0/previews/slide-deck-04.png differ diff --git a/templates/step0/previews/slide-deck-05.png b/templates/step0/previews/slide-deck-05.png new file mode 100644 index 000000000..52a59fae3 Binary files /dev/null and b/templates/step0/previews/slide-deck-05.png differ diff --git a/templates/step0/previews/slide-deck-06.png b/templates/step0/previews/slide-deck-06.png new file mode 100644 index 000000000..0c3fc1ee6 Binary files /dev/null and b/templates/step0/previews/slide-deck-06.png differ