跳转至

Deck SKILL — 滚动叙事 PPT 完整手册

本文件合并自 v0.2 的 SKILL.md(视频处理)、SKILL-scene-anatomy.md(背景图位置)、SKILL-style.md(视觉风格)。

出处

源自 deck 项目 v0.3(2026-05-20),随本 wiki 作为活手册维护;制作背景见《会动的网页 PPT 是怎么做出来的》,可运行的 deck 真身在其 assets/deck/

目录

  • 一、Scroll-Driven Video — MP4 处理(faststart / all-intra 诊断与修复、批量脚本、前端要点、循环背景)
  • 二、Scene Anatomy — 背景图位置速查(九宫格速查表、情绪关键词、高频陷阱)
  • 三、Deck Style Guide — 视觉风格规范(色彩 / 字体 / 8 种 layout / 数据驱动架构 / 反例)

一、Scroll-Driven Video — MP4 处理

适用场景

任意"滚动进度 → video.currentTime"的网页。视频不是用 play() 播放,而是赋值 currentTime 让画面跟随滚动逐帧切换

普通导出的 MP4 在这种用法下几乎一定有问题,必须做两步处理:faststart + all-intra

1.1 moov atom 位置(faststart)

症状: - 视频要等几乎全部下载完才能 seek 到后面的帧 - 滚得太快时后半段直接跳到末尾 - 网络慢时整段画面卡在开头

原理: MP4 由 ftyp(文件类型)、moov(索引)、mdat(视频数据)组成。大多数编码器默认把 moov 放在 mdat 之后(文件末尾)。浏览器拿到 moov 之前不知道任意 currentTime 对应哪段字节,只能等下完整个文件才能精确 seek。

诊断:

python3 -c "
import sys
def find_atoms(path):
    with open(path, 'rb') as f: data = f.read()
    i = 0
    while i + 8 < len(data):
        size = int.from_bytes(data[i:i+4], 'big')
        atype = data[i+4:i+8].decode('ascii', errors='replace')
        print(atype, 'offset=', i, 'size=', size)
        if size < 8: break
        i += size
find_atoms(sys.argv[1])
" your.mp4
如果 moov 出现在 mdat 之后,就是问题。

修复(不重编码,秒级完成):

ffmpeg -y -i input.mp4 -c copy -movflags +faststart output.mp4

1.2 Keyframe 密度(all-intra 重编码)

症状: - 视频只播开头一小段然后画面冻结或直接跳到末尾 - 即使做了 faststart 也没改善 - 快速滚动时画面"跳帧",看不到中间帧

原理: 只有 keyframe (I-frame) 能被独立解码。中间帧 (P/B-frame) 必须依赖最近的 keyframe 顺序解码。常规编码 GOP 通常 1–10 秒一个 keyframe,5 秒短视频可能只有 1–2 个。Scroll-driven 场景下浏览器每帧都要 seek 到任意时间点,非 keyframe 位置根本来不及解码,直接放弃。

诊断:

# 关键帧数(应该 ≈ 总帧数)
ffprobe -v error -select_streams v:0 \
  -show_entries packet=pts_time,flags \
  -of csv=p=0 your.mp4 | grep -c K

# 总帧数
ffprobe -v error -select_streams v:0 -count_frames \
  -show_entries stream=nb_read_frames -of csv=p=0 your.mp4

修复(必须重编码):

ffmpeg -y -i input.mp4 \
  -c:v libx264 -preset fast -crf 20 \
  -g 1 -keyint_min 1 -sc_threshold 0 \
  -pix_fmt yuv420p -an \
  -movflags +faststart \
  output.mp4

参数 作用
-g 1 -keyint_min 1 每帧都是 keyframe(GOP = 1)
-sc_threshold 0 关闭场景切换检测
-crf 20 视觉无损偏上质量,可调 18–23
-pix_fmt yuv420p 确保所有浏览器兼容(Safari 不吃 yuv444p)
-an 删除音轨
-movflags +faststart 顺手把 moov 放开头

代价: 文件大小 1.5–3 倍,但 scroll-driven 视频一般只有几秒,绝对体积仍很小(3–8 MB)。

1.3 一键修复脚本

同时解决 faststart + all-intra:

ffmpeg -y -i in.mp4 \
  -c:v libx264 -preset fast -crf 20 \
  -g 1 -keyint_min 1 -sc_threshold 0 \
  -pix_fmt yuv420p -an \
  -movflags +faststart \
  out.mp4

1.4 批量修复(v0.2 实战经验)

每次加入新的过渡视频都必须跑一遍。 v0.2 开发中 trans6to7 ~ trans11to11 共 6 个视频全部需要重编码,原始素材无一例外都是 1 个 keyframe / 97~121 帧。养成习惯:素材入库时立即诊断 + 修复,不要等到浏览器里发现"卡住了"再回头查。

批量脚本:

cd ./素材
for f in trans*.mp4; do
  kf=$(ffprobe -v error -select_streams v:0 \
    -show_entries packet=pts_time,flags -of csv=p=0 "$f" | grep -c K)
  total=$(ffprobe -v error -select_streams v:0 -count_frames \
    -show_entries stream=nb_read_frames -of csv=p=0 "$f")
  if [ "$kf" -lt "$total" ]; then
    echo "$f: $kf/$total keyframes — fixing..."
    mv "$f" "${f%.mp4}_orig.mp4"
    ffmpeg -y -i "${f%.mp4}_orig.mp4" \
      -c:v libx264 -preset fast -crf 20 \
      -g 1 -keyint_min 1 -sc_threshold 0 \
      -pix_fmt yuv420p -an \
      -movflags +faststart "$f" 2>/dev/null
  else
    echo "$f: $kf/$total — OK"
  fi
done

1.5 前端配套要点

video 元素属性:

<video src="./clip.mp4" muted playsinline preload="auto"></video>
- 必须muted playsinline preload="auto" - 不要autoplay controls loop(loop 仅用于循环背景视频) - iOS Safari 必须 playsinline

滚动驱动逻辑: 本项目用 all-intra 编码后 currentTime 直接赋值(不缓动),因为每帧都是 keyframe,不存在 seek 延迟。如果没用 all-intra,需要 rAF 缓动逼近(系数 0.4)。

必须用 HTTP server: 浏览器对 file:// 协议下的视频 seek 限制很严。本地预览用 python3 -m http.server 8000

1.6 排查清单

  1. 通过 HTTP server 访问(不是 file://
  2. 浏览器硬刷新清缓存(Ctrl + Shift + R)—— 改完视频不刷新最容易踩
  3. moov 在文件开头 → 否则 ffmpeg -c copy -movflags +faststart
  4. keyframe 数 ≈ 总帧数 → 否则 -g 1 -keyint_min 1 -sc_threshold 0 重编码
  5. 前端:muted playsinline preload="auto"loadedmetadata 后才读 duration

1.7 循环动画背景

当过渡视频的首帧 == 尾帧时,可作为无限循环背景:

  1. content.js 的 media 数组中,将该项声明为 { type: 'video', id: 'sN', src: '...', loop: true }
  2. deck.js 会自动识别 loop: true 的 media 并调用 .play()
  3. 支持多个循环视频(v0.2 有 2 个:标题页 trans1to2 + 结尾页 trans11to11)

本项目实例: - trans1to2.mp4(首尾帧 = scene1/scene2 同一张图)→ 标题页无限循环背景 - trans11to11.mp4(首尾帧 = scene11 同一张图)→ 结尾「感谢聆听」无限循环背景


二、Scene Anatomy — 柴犬背景图位置速查

所有图都是 16:9 灰底,柴犬主体占 1/3 ~ 1/2 画面,剩余大量留白。 文字必须落在留白里,盖到柴犬脸上 = 设计失败。 用 3×3 网格描述位置:TL/TM/TR · ML/MC/MR · BL/BM/BR。

2.1 速查表

scene 柴犬动作 / 配饰 主体位置 安全文字区 推荐 layout
0 趴 + 绿蝴蝶结 BM 上半 / 左下角 hero
1 奔跑 BM 左半 / 右上 left
2 奔跑(同 scene1) BM 左半 / 大字压底 anchor
3 害羞坐 + 樱花 + 桃心 BR 左半 + 上半 left
4 戴黄帽倒立 + 花盆 BL 右半 right
5 玩绿色毛线球 MR–BR 左半 anchor
6 仰躺玩耍 TL–ML 右下 2/3 right
7 cookies 盒 + 蓝领带 MR–BR 左半 anchor
8 趴 + 黄花 + 左侧桃心 BL–ML 右半 + 中心 quote
9 站立鼓掌 + 黄星 ML–MC 右半 + 下半 right
10 抱刺猬 + 桃心 MR–BR 左半 left
11 戴皇冠 zzz BM(偏右) 上半 + 左半 hero

2.2 情绪关键词

scene 关键词 情绪
0 静、安、待出发 开场
1 动、轻快 起跑 / 新章节
2 动、连续 转场
3 含蓄、害羞 犹豫选择
4 倒挂、不寻常 早期探索
5 好奇、捣鼓 养一个 Agent
6 慵懒、放松 日常使用
7 享受、满足 工程化就位
8 安静、专注 深入讲解
9 振奋、被鼓励 做一个项目
10 拥抱、温柔反思 复盘 + 延伸
11 安睡、戴冠 收尾

2.3 三类高频陷阱

  1. 左对齐文字 + 柴犬在左 = 盖脸 → 用 layout-right
  2. 大字压底 + 柴犬也在底 = 互怼 → 用 layout-left / layout-right 让文字上提
  3. 底部章节条 + col-stats 都在底 = 拥挤 → layout-hero 把 stats 横向居中

2.4 v0.2 实际背景分配

scene 图 内容 备注
s0 scene0 开场等待
s1 trans1to2 循环 标题页 scene1=scene2,用视频循环
s2 scene3 Part 1 章节页
s3–s6 scene4 Part 1 四个手风琴 共享背景
s7 scene5 Part 2 章节页
s8–s10 scene6 Part 2 三个手风琴 共享背景
s11 scene7 Part 3 章节页
s12 scene8 Harness 阶梯页
s13 scene9 如何 Harness
s14 scene10 做一个项目
s15 scene10 两条沉淀 与 s14 共享
s16 trans11to11 循环 感谢聆听 scene11 仅用于循环动画首尾帧

scene11.png 不作为静态背景使用,它只是用于生成 trans11to11 循环视频的首尾帧。

2.5 如果换图

换图后本表要同步更新。流程:开一张新图 → 先想"柴犬占哪个九宫格" → 再决定走哪种 layout。


三、Deck Style Guide — 视觉风格规范

灵感来源:Brandly 风格 hero 首屏。 关键词:编辑感 · 字号说话 · 大留白 · 单点橙色强调

3.1 色彩系统

所有颜色定义在 :root,改全局色调只改这里:

token 用途
--ink #0a0a0a 正文 / 标题 / 边框
--paper #e8e6e1 兜底背景(被 scene 图盖住)
--letterbox #000 16:9 stage 外的黑色信封边
--accent #ff6a1a 唯一强调色 · 橙
--muted rgba(10,10,10,0.58) 二级文字
--hairline rgba(10,10,10,0.18) 细分隔线

橙色用在哪: 标题里 <span class="accent"> 包住的一两个字、当前章节胶囊 active 时。仅此而已。

3.2 字体系统

字体 用途 权重
Archivo Black 主标题 / logo / 大数字 900
Noto Sans SC 所有中文 400 / 500 / 700 / 900
Inter 西文正文 / 导航 / 注释 400 / 500 / 600 / 700

铁律: - 行高 0.9–0.94,视觉重量靠这个 - 主标题 letter-spacing 必须负(-0.02 ~ -0.03em) - kicker / footer-meta 用 text-transform: uppercase + letter-spacing 0.18–0.28em

3.3 版式骨架

  • 16:9 letterbox,container-type: size,子元素用 cqw / cqh
  • .scene padding:14cqh 3cqw 10cqh(顶部留 nav,底部留章节条)

3.4 八种 layout

class 几何特征 何时用
.layout-blank 无内容 开场等待页
.layout-hero 单列居中,巨大标题 开场 / 结尾 / 重磅页
.layout-left 文字左 60% + stats 右 30% 柴犬在右半时
.layout-right 文字右 60%(右对齐) 柴犬在左半时
.layout-anchor 标题撑满 + 侧边 modules 章节封面
.layout-accordion 左 overview + 右 case 卡片 多案例并列展示
.layout-steps 左侧 4 步阶梯 + 右侧大标题 有顺序的递进关系
.layout-dual 双栏对比 + 底部 footer 两个概念并列对比

3.5 数据驱动架构(v0.2 起)

所有内容在 content.jswindow.CONTENT 对象中声明,layouts.js 负责渲染,deck.js 负责滚动和交互。加一页只需:

  1. content.jsmedia 数组追加背景图/视频(新视频必须先跑 all-intra 修复)
  2. content.jsscenes 数组追加 scene 对象,指定 layoutchapter
  3. 标题里挑一个词包 <span class="accent">
  4. 如需新章节:更新 nav + chapters 数组
  5. segmentssceneToChapter 自动计算,不需手动改

3.6 留白节奏

  • nav 离顶 2.6cqh,章节条离底 4cqh
  • 列间距 3cqw,行间距 3cqh
  • kicker→headline 1.2cqh,headline→subhead 2.4cqh

宁可大,不要小。 呼吸感全靠 padding 够狠。

3.7 反例(请勿做)

  • 给段落整体上橙色(强调色失效)
  • .stat 加圆角卡片底(变 dashboard)
  • headline 行高 > 1.2(丢视觉重量)
  • 同一 scene 用 2+ 个 <span class="accent">(焦点散)
  • cqw 改成 vw(响应式会爆)