会动的网页 PPT 是怎么做出来的
2026 年 5 月我做了一次组内分享,没用 PPT 软件——演示稿是一个滚动驱动的叙事网页:向下滚动,背景里的柴犬在 12 个场景之间连续动画过渡,文案随场景切换。先看效果:
操作方式
先点一下画面,然后按下鼠标右键,尽量别用滚轮
原理:静帧 + 过场视频的三明治
拆开看只有三种东西:
页面本体是一个很高的空滚动区(数百 vh),真正的画面固定在视口里。滚动进度落在静帧段就显示对应 PNG;落在过渡段就把滚动进度映射成视频的 currentTime——不是 play() 播放,而是滚多少、视频走多少帧,倒着滚就倒放。因为过场视频的首帧和尾帧就是前后两张静帧图,衔接处像素级无缝,看起来就是「一镜到底」。
素材管线:关键帧图 + 首尾帧视频
第一步,出关键帧。 12 个场景 = 12 张柴犬图,用 GPT 网页批量生成 image2 再人工筛选。出图不是随便出,有三条规矩(来自工程手册 deck skill 的 Scene Anatomy 章):
- 统一 16:9 灰底,柴犬主体只占 1/3 ~ 1/2 画面,剩余大量留白是给文字的——文字盖到柴犬脸上就是设计失败;
- 每张图先想清楚「柴犬落在九宫格哪个位置」,再决定这一页文字走哪种版式;
- 每个场景对应叙事弧线上的一个情绪:待出发 → 起跑 → 犹豫 → 探索 → 好奇 → 慵懒 → 满足 → 专注 → 振奋 → 温柔反思 → 安睡戴冠。
当时的调研笔记里有一句现在看依然成立的话:
我发现里面最难的还是美术风格,即便你知道使用什么技术,里面也会有很多坑……特别是这种非常主观表达的场景,可以说是有一部分创作的成分在里面,我们需要的并不是一个 AI 非常无偏的表达。
所以「批量出图 + 人工筛选」不是偷懒,它就是这类主观创作场景下和图像模型的正确协作方式——模型负责量,人负责挑出符合叙事情绪的那张。
第二步,首尾帧生成过场视频。 把相邻两张关键帧作为首帧和尾帧喂给支持首尾帧的图生视频模型,让它脑补中间的运动(成片约 4 秒、720p、24fps)。这一招还有个彩蛋用法:首尾给同一张图,就得到一段可以无限循环的动画背景——标题页(trans1to2,奔跑循环)和结尾「感谢聆听」页(trans11to11,安睡循环)都是这么来的,静帧页里柴犬是活的。
素材目录里至今留着管线化石:_orig.mp4 是模型原片,_抠图.png 是一轮没用上的抠图实验——创作过程比成品目录乱得多,这很正常。
最大的坑:模型给的 mp4 根本没法 scroll-seek
这是整个项目踩得最深的坑,也是这篇文章最值钱的一节。视频模型(以及几乎一切常规导出)给你的 mp4,直接拿来做 scroll-driven 必然出现:滚到中间画面冻住、只播开头然后直接跳末尾、必须滚很久才开始动。两个原因:
moov索引在文件末尾。 mp4 的moovatom 记录「每个时间点对应文件哪个字节」,多数编码器默认把它放在文件尾部——浏览器没拿到索引之前,任何currentTime都无法精确 seek,只能等整个文件几乎下完。- 关键帧密度不够,这个更致命。 视频里只有 keyframe(I-frame)能独立解码,常规编码几秒才一个。scroll-driven 是每一帧
requestAnimationFrame都要 seek 到任意时间点——目标不是 keyframe 时,浏览器要找到上一个 keyframe 顺序解码过来,根本来不及,于是放弃、渲染手头最近的帧(往往是开头或末尾)。实测 v0.2 新入库的 6 段过场,无一例外都是 1 个 keyframe / 97~121 帧。
修复一条 ffmpeg 命令同时解决两个问题——重编码成每帧都是关键帧(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
-g 1 就是「GOP = 1,每帧皆 keyframe」。代价是体积变为 1.5~3 倍,但几秒的过场绝对体积仍然很小,换来帧级精确 seek。经验教训是把它做成素材入库仪式:每段新视频进目录先跑诊断(keyframe 数应 ≈ 总帧数),不达标立即重编码,不要等浏览器里「卡住了」再回头查——诊断命令和批量脚本都在工程手册里。
前端配套三件套也别漏:<video muted playsinline preload="auto">(iOS Safari 没有 playsinline 直接不渲染);本地预览必须走 HTTP server(file:// 下浏览器不给视频 byte-range);改完视频记得硬刷新清缓存——最后这条最蠢也最常踩。
引擎:三层 js,数据驱动
整个引擎没有框架,三个文件各管一层:
| 文件 | 职责 |
|---|---|
content.js |
全部文案和媒体声明,一个大对象,改内容只碰这里 |
layouts.js |
8 种版式的渲染函数(hero / left / right / anchor / accordion / steps / dual / blank) |
deck.js |
滚动映射、视频 seek、手风琴与画廊交互 |
这个分层的直接收益是加一页只要在 content.js 追加一个对象:声明背景(新视频先过 all-intra 仪式)、挑版式、标题里选一个词包 <span class="accent">,段落区间和章节归属全部自动计算。文案单源也意味着 AI 改稿只需要读写一个文件——这份稿子后期的内容调整基本都是这么让 AI 干的。
版式层面有几条铁律,来自工程手册的 Style Guide 章,违反任何一条页面气质就塌:
- 每页只允许一个橙色强调词(
#ff6a1a是全局唯一强调色,多用即失效); - 主标题行高 0.9~0.94、letter-spacing 必须为负——视觉重量全靠这两个参数;
- 16:9 letterbox 内一律用容器查询单位
cqw/cqh,写成vw响应式直接爆; - 留白宁大勿小,stats 是纯文字大数字,加圆角卡片底就变 dashboard 了。
把坑沉淀成 SKILL,让 AI 下次不再踩
这个项目从 v0.1 迭代到 v0.3,目录里最有价值的演变不是代码,是文档:v0.1 时视频处理、场景排布、视觉风格是三份散的笔记,v0.3 合并成一份带 frontmatter 的 SKILL.md——「新人看完就能改内容、加页、换图、排查视频问题」,而这个「新人」主要是 AI。下次再做同类 deck,把手册喂进上下文,all-intra、九宫格、单点橙这些坑一个都不用重踩。
另外存档了一份更早的化石:prompt-v0.md,第一版滚动开场的一次成型 prompt——从资产清单、滚动区间映射表到「scroll 事件只更新目标值、rAF 里缓动 seek」的实现要点全部写死在 prompt 里,AI 一发直出完整 HTML。对比它和后来的三层引擎能看出这类项目的自然演化:一次性 prompt 长出可维护的架构,踩坑记录长出 SKILL。
附:怎么原样搬进这个 wiki
这个 deck 不是单文件——入口 html + 3 js + 1 css + 40 多个媒体文件,内部全是相对路径。能整体进 wiki 靠的是本站部署链路对静态文件的原样透传:zensical 构建时 docs/ 里所有非 .md 文件(含 .html)原样拷进 site/,deploy.sh 再把 site/ 整体镜像到 COS——对象存储不关心你放的是一篇页面还是一整个网页应用。搬运时动了四处:
- 只搬运行时子集:历史版本、
_orig原片、讲稿.md都不带——.md混进docs/会被当 wiki 页面渲染; - 字体本地化:原版走 Google Fonts,国内不可达会卡渲染。把 Archivo Black + Inter 的 woff2 真身下进
fonts.css(共约 160KB),中文走系统字体兜底; - 摘除内网 iframe:「在线试玩」原来嵌的是公司内网地址,公网必挂且泄漏 IP,换成说明文字;
- 摘除失效外链:一处封面图是 star-history 在线图表,该 API 已因 GitHub 限制 star 数据而失效(原版现在也是裂的),移除后手风琴直接展开案例。
本地预览的坑:zensical serve 是简易开发服务器,会被 deck 一次性预加载的 12 段视频并发连接压挂(之后所有请求永远 pending)——预览这页请用 npx serve site;线上 COS 原生支持 Range 和并发,无此问题。
附:源码与手稿原文
引擎真身在本文 assets/deck/ 下,线上有独立 URL 可直接 GET;此处按规矩折叠展示,方便就地查看。工程手册已沉淀为独立 skill——滚动 deck 工程手册(Skills · 写作口味,导航可达),不在此重复。
引擎五件套(wiki 发布版:字体已本地化、内网 iframe 与失效外链已摘除,均留有注释):
index.html —— 入口骨架(21 行,画面全由三层 js 生成)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>AI 工具实践分享</title>
<!-- 字体真身随站发布(原 Google Fonts 国内不可达);中文走 PingFang/雅黑系统兜底 -->
<link rel="stylesheet" href="fonts.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="stage">
<div class="stage-inner" id="stageInner" data-active="0"></div>
</div>
<div class="spacer"></div>
<div class="global-timer" id="globalTimer">00:00</div>
<!-- 全屏按钮(wiki 发布版新增):文章内嵌 iframe 与新标签页都可一键进入沉浸阅读 -->
<button class="fs-btn" id="fsBtn" title="全屏切换(快捷键 F)">⛶ 全屏</button>
<style>
.fs-btn {
position: fixed; bottom: 1.2vh; right: 1.2vw; z-index: 9999;
font: 600 max(1.1vw, 12px)/1 'Inter', 'Noto Sans SC', sans-serif;
letter-spacing: 0.14em;
color: rgba(10, 10, 10, 0.58); background: none;
border: 1px solid rgba(10, 10, 10, 0.18); border-radius: 999px;
padding: 0.6em 1.1em; cursor: pointer;
}
.fs-btn:hover { color: #ff6a1a; border-color: #ff6a1a; }
</style>
<script>
(() => {
const btn = document.getElementById('fsBtn');
const root = document.documentElement;
const toggle = () => document.fullscreenElement || document.webkitFullscreenElement
? (document.exitFullscreen || document.webkitExitFullscreen).call(document)
: (root.requestFullscreen || root.webkitRequestFullscreen).call(root);
btn.addEventListener('click', toggle);
// 快捷键 F 由 deck.js 原生处理,此处不再监听(避免双触发互相抵消)
document.addEventListener('fullscreenchange', () => {
btn.innerHTML = document.fullscreenElement ? '⛶ 退出全屏' : '⛶ 全屏';
});
})();
</script>
<script src="content.js"></script>
<script src="layouts.js"></script>
<script src="deck.js"></script>
</body>
</html>
content.js —— 全部文案与媒体声明,改内容只碰这里(单源)
window.CONTENT = {
meta: {
title: 'AI 工具实践分享 · v0.3',
version: 'v0.3',
date: '2026.05.21',
logo: 'Tap4Fun.BI',
},
media: [
{ type: 'image', id: 's0', src: './素材/scene0.webp' },
{ type: 'video', id: 'v0_1', src: './素材/trans0to1.mp4' },
{ type: 'video', id: 's1', src: './素材/trans1to2.mp4', loop: true },
{ type: 'video', id: 'v1_2', src: './素材/trans2to3.mp4' },
{ type: 'image', id: 's2', src: './素材/scene3.webp' },
{ type: 'video', id: 'v2_3', src: './素材/trans3to4.mp4' },
{ type: 'image', id: 's3', src: './素材/scene4.webp' },
{ type: 'image', id: 's4', src: './素材/scene4.webp' },
{ type: 'image', id: 's5', src: './素材/scene4.webp' },
{ type: 'image', id: 's6', src: './素材/scene4.webp' },
{ type: 'video', id: 'v6_7', src: './素材/trans4to5.mp4' },
{ type: 'image', id: 's7', src: './素材/scene5.webp' },
{ type: 'video', id: 'v7_8', src: './素材/trans5to6.mp4' },
{ type: 'image', id: 's8', src: './素材/scene6.webp' },
{ type: 'image', id: 's9', src: './素材/scene6.webp' },
{ type: 'image', id: 's10', src: './素材/scene6.webp' },
{ type: 'video', id: 'v10_11', src: './素材/trans6to7.mp4' },
{ type: 'image', id: 's11', src: './素材/scene7.webp' },
{ type: 'video', id: 'v11_12', src: './素材/trans7to8.mp4' },
{ type: 'image', id: 's12', src: './素材/scene8.webp' },
{ type: 'video', id: 'v12_13', src: './素材/trans8to9.mp4' },
{ type: 'image', id: 's13', src: './素材/scene9.webp' },
{ type: 'video', id: 'v13_14', src: './素材/trans9to10.mp4' },
{ type: 'image', id: 's14', src: './素材/scene10.webp' },
{ type: 'image', id: 's15', src: './素材/scene10.webp' },
{ type: 'video', id: 'v15_16', src: './素材/trans10to11.mp4' },
{ type: 'video', id: 's16', src: './素材/trans11to11.mp4', loop: true },
],
nav: [
{ label: 'Prologue', jump: 0 },
{ label: 'Title', jump: 1 },
{ label: 'Part 1', jump: 2 },
{ label: 'Part 2', jump: 7 },
{ label: 'Part 3', jump: 11 },
{ label: 'Part 4', jump: 14 },
{ label: 'Fin', jump: 16 },
],
chapters: [
{ mark: 'circle', label: 'Prologue' },
{ mark: 'square', label: 'Part 1' },
{ mark: 'tri', label: 'Part 2' },
{ mark: 'bar', label: 'Part 3' },
{ mark: 'dot', label: 'Part 4' },
{ mark: 'circle', label: 'Fin' },
],
scenes: [
{
layout: 'blank',
chapter: 0,
},
{
layout: 'hero',
chapter: 0,
classes: ['scene-title'],
kicker: 'AI 工具实践分享',
headlineMid: '今天到底该',
headline: '用什么 <span class="accent">AI</span> 工具',
subhead: '从日常提效到 Agent 工程化探索',
},
{
layout: 'anchor',
chapter: 1,
kicker: 'Part 01 · Quick Tricks',
headline: '看了就能<br><span class="accent">变强</span>的小技巧',
subhead: '<strong>技多不压身,学到就是赚到。</strong><br><span style="color: var(--muted);">目标:会后就能拿走几个真实可用的方法。</span>',
modules: [
{ num: '4-1', name: 'Google Sheet', tag: '脚本 · 节假日 · 翻译' },
{ num: '4-2', name: '网页 AI', tag: 'DeepSeek · AI Studio · GPT' },
{ num: '4-3', name: 'AI PPT', tag: '工作流 · Skill · 美术' },
{ num: '4-4', name: '浏览器 / 电脑', tag: 'Browser Use · Computer Use' },
],
},
{
layout: 'accordion',
chapter: 1,
accId: 'acc-1',
coverImg: './part1/app脚本.jpg',
overview: {
kicker: '4-1 · Google Sheet · 小技巧',
title: '第一部分<br><span class="accent">Google Sheet</span> 小技巧',
lead: '围绕 Google Sheet 的自定义 App 脚本,解决日常小痛点。',
note: '<span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Case 01',
title: '翻译脚本',
summary: '翻译脚本与结果对照',
detail: {
title: '翻译脚本与<span class="accent">结果对照</span>',
paragraphs: [
'<video src="./part1/翻译脚本效果.mp4" controls playsinline class="detail-media"></video>',
],
codeFile: './part1/翻译脚本.js',
aside: [
{ kicker: '类型', text: 'Google Sheet <strong>App 脚本</strong>' },
],
},
},
{
tag: 'Case 02',
title: '节假日统计',
summary: '节假日统计脚本',
detail: {
title: '节假日<span class="accent">统计</span>',
paragraphs: [
'<img src="./part1/节假日.jpg" class="detail-media">',
],
codeFile: './part1/节假日.js',
aside: [
{ kicker: '类型', text: 'Google Sheet <strong>App 脚本</strong>' },
],
},
},
{
tag: 'Case 03',
title: '更新通知整理',
summary: '更新通知整理',
detail: {
title: '更新通知<span class="accent">整理</span>',
paragraphs: [
'<img src="./part1/app脚本.jpg" class="detail-media">',
],
aside: [
{ kicker: '类型', text: 'Google Sheet <strong>App 脚本</strong>' },
],
},
},
],
},
{
layout: 'accordion',
chapter: 1,
accId: 'acc-2',
coverImg: './part1/网页端占比.png',
overview: {
kicker: '4-2 · 网页 AI',
title: '不同任务<br>选不同<span class="accent">工具</span>',
lead: '每个 AI 产品都有擅长的边界,挑对工具效率翻倍。',
note: '<span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Case 01',
title: 'DeepSeek',
summary: 'RP 特定提示词',
detail: {
title: 'DeepSeek V4 <span class="accent">RP</span> 思考模式切换',
paragraphs: [
'<strong>三种模式:</strong>默认(自动选择)/ <span class="accent-text">角色沉浸</span>(带内心独白)/ 纯分析(冷静规划)',
'<code class="detail-code">角色沉浸模式: 纯分析模式:\n<think> <think>\n(他跟我打招呼了……心跳加速。) 场景:用户打招呼,角色是傲娇属性。\n我要装作不在意的样子回应。 回复策略:先嫌弃,身体语言暴露真情。\n(不能让他看出来我很高兴!) 控制150字,先动作描写再对话。\n</think> </think></code>',
'<strong>用法:</strong>在第一条消息末尾粘贴指令,后续正常聊天即可全程生效。',
],
aside: [
{ kicker: '仓库', text: '<a href="https://github.com/victorchen96/deepseek_v4_rolepaly_instruct/tree/main" target="_blank" style="color:var(--accent);font-weight:600;">deepseek_v4_roleplay_instruct →</a>' },
{ kicker: '适用', text: 'DeepSeek 官方 APP / 网页<strong>专家模式</strong>,及 deepseek-v4-flash / pro API' },
{ kicker: '注意', text: '概率输出,无法 100% 触发,可多 roll 几次' },
],
},
},
{
tag: 'Case 02',
title: 'AI Studio',
summary: '优先于 Gemini 网页',
detail: {
title: '<span class="accent">AI Studio</span> 优先于 Gemini 网页',
paragraphs: [
'<div class="detail-media-row"><img src="./part1/aistudio.jpg" class="detail-media"><img src="./part1/gemini会员.jpg" class="detail-media"></div>',
],
aside: [
{ kicker: '链接', text: '<a href="https://aistudio.google.com/" target="_blank" style="color:var(--accent);font-weight:600;">aistudio.google.com →</a>' },
{ kicker: '说明', text: 'Gemini Pro 会员可以用<strong>更多功能</strong>' },
],
},
},
{
tag: 'Case 03',
title: 'GPT',
summary: '大量 image2 生成',
detail: {
title: 'GPT 网页适合<br>大量 <span class="accent">image2</span> 生成',
paragraphs: [
'<img src="./part1/gpt网页.jpg" class="detail-media">',
],
aside: [
{ kicker: '适合', text: '大量图片生成场景' },
],
},
},
{
tag: 'Case 04',
title: 'Claude',
summary: '',
detail: {
title: '<span class="accent">Claude</span>',
reveal: true,
paragraphs: [
'<div class="reveal-wrap"><img src="./part1/claude.webp" class="detail-media reveal-img"></div>',
],
aside: [],
},
},
],
},
{
layout: 'accordion',
chapter: 1,
accId: 'acc-3',
overview: {
kicker: '4-3 · AI PPT',
title: '<span class="accent">AI PPT</span>',
lead: '工作流平台 + PPT Skill,两条技术路线。',
note: '<span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Case 01',
title: '工作流',
summary: 'Genspark / Coze / LandPPT',
detail: {
title: '<span class="accent">工作流</span>',
paragraphs: [
'<a href="https://www.genspark.ai/" target="_blank" class="detail-link">Genspark →</a>',
'<a href="https://www.coze.cn/overview" target="_blank" class="detail-link">Coze →</a>',
'<a href="https://github.com/sligter/LandPPT" target="_blank" class="detail-link">LandPPT (GitHub) →</a>',
],
aside: [
{ kicker: '类型', text: '端到端 <strong>AI PPT 工作流</strong>平台' },
],
},
},
{
tag: 'Case 02',
title: 'PPT Skill',
summary: '越来越流行',
detail: {
title: '<span class="accent">PPT Skill</span><br>越来越流行',
paragraphs: [
'<a href="https://github.com/mucsbr/ppt-agent-workflow-san" target="_blank" class="detail-link">ppt-agent-workflow-san →</a>',
'<a href="https://github.com/JuneYaooo/gpt-image2-ppt-skills" target="_blank" class="detail-link">gpt-image2-ppt-skills →</a>',
'<a href="https://github.com/stevenjinlong/awesome-ppt-skills" target="_blank" class="detail-link">awesome-ppt-skills →</a>',
],
aside: [
{ kicker: '趋势', text: '大模型 + PPT Skill,<strong>越来越流行</strong>' },
],
},
},
],
},
{
layout: 'accordion',
chapter: 1,
accId: 'acc-4',
overview: {
kicker: '4-4 · 浏览器操作与电脑操作',
title: '浏览器操作<br>与<span class="accent">电脑</span>操作',
lead: 'AI 开始能操作界面,但都还在早期。',
note: '<span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Case 01',
title: '操作浏览器',
summary: 'Browser Use',
detail: {
title: '操作<span class="accent">浏览器</span>',
paragraphs: [
'<a href="https://github.com/iFurySt/open-browser-use" target="_blank" class="detail-link">Codex 浏览器拆解 · open-browser-use →</a>',
],
aside: [
{ kicker: '类型', text: '<strong>Browser Use</strong> 开源拆解' },
],
},
},
{
tag: 'Case 02',
title: '操作电脑',
summary: 'Computer Use',
detail: {
title: '操作<span class="accent">电脑</span>',
paragraphs: [
'<span style="font-weight:600;">perplexity / sonar-pro-search</span>',
'<a href="https://github.com/iFurySt/open-codex-computer-use" target="_blank" class="detail-link">Codex Computer Use 拆解 · open-codex-computer-use →</a>',
],
aside: [
{ kicker: '类型', text: '<strong>Computer Use</strong> 开源拆解' },
],
},
},
],
},
{
layout: 'anchor',
chapter: 2,
kicker: 'Part 02 · OpenClaw',
headline: '自己养一个<br><span class="accent">Agent</span>',
subhead: '<strong>Agent 不只是会聊天,而是能稳定接任务、执行任务、反馈状态。</strong><br><span style="color: var(--muted);">长期运行的 Agent,需要边界、维护和监控。</span>',
modules: [
{ num: '01', name: '运维脚本', tag: '自动化入口' },
{ num: '02', name: '角色扮演', tag: 'RP 实验场' },
{ num: '04', name: '信息收集', tag: '邮件 · 播客 · 摘要' },
{ num: '03', name: '信息输出', tag: 'HTML · 卡片 · 图' },
],
},
{
layout: 'accordion',
chapter: 2,
classes: ['ov-bottom'],
accId: 'acc-5',
overview: {
kicker: '6-1 · OpenClaw · 实验矩阵',
title: '功能到<span class="accent">产品</span><br>很远',
lead: 'AI 能力越强,人越需要做产品判断。',
note: '图像生成有能力,不等于朋友圈场景、姿势、穿搭、多样性都自动解决。<br><br><span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Exp 01',
title: '祖师对话',
summary: 'RP 模拟历史人物',
detail: {
title: '佛教<span class="accent">祖师</span>对话',
paragraphs: [
'<span class="detail-big">通过聊天框和祖师对话</span>',
'<span class="detail-mid">比直接进入网页更有<strong>和人对话的感觉</strong></span>',
'<span class="detail-mid"><strong class="accent-text">14 位祖师</strong> — 慧能 · 玄奘 · 宗喀巴 · 米拉日巴 · 阿姜查 …<br>覆盖汉传 / 藏传 / 南传三大传统</span>',
'<span class="detail-mid"><strong class="accent-text">503 数据源</strong> · 67.8 万+ 语义向量<br>每条回答必须引用可查证经典,杜绝"编经"</span>',
],
aside: [
{ kicker: '体验', text: '<a href="https://fojin.app/chat" target="_blank" style="color:var(--accent);font-weight:600;">fojin.app/chat →</a>' },
{ kicker: '仓库', text: '<a href="https://github.com/xr843/Master-skill" target="_blank" style="color:var(--accent);font-weight:600;">Master-skill (GitHub) →</a>' },
{ kicker: '接入', text: 'Claude Code / Cursor / Gemini CLI,一条命令安装' },
],
},
},
{
tag: 'Exp 02',
title: '睡前故事',
summary: '内容生成',
detail: {
},
},
{
tag: 'Exp 03',
title: '塔罗牌',
summary: '个性化报告',
detail: {
},
},
{
tag: 'Exp 04',
title: 'AI 小说',
summary: '长篇连载',
detail: {
},
},
{
tag: 'Exp 05',
title: '自拍',
summary: '角色自拍图 + 文案',
detail: {
title: '角色<span class="accent">朋友圈</span>',
paragraphs: [
'生成虚拟角色自拍图和文案,模拟真实社交动态。',
],
aside: [
{ kicker: '暴露', text: '姿势 · 穿搭 · 多样性<strong>全是坑</strong>。' },
],
},
},
],
},
{
layout: 'accordion',
chapter: 2,
classes: ['ov-bottom'],
accId: 'acc-7',
overview: {
kicker: '6-3 · 邮件系统 · 信息入口',
title: '重要的不是<br><span class="accent">全都接收</span>',
lead: '让 AI 负责信息收集整理,但不要把所有信息直接倒给人。',
note: '每个人产生的信息越来越多,重要的不是"全都接收",而是"<strong>哪些值得看</strong>"。<br>筛选能力应该放在上层,而不是把原始信息直接发给上层。<br><br><span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'System 01',
title: '邮件系统',
summary: '信息入口 + 筛选',
detail: {
title: '<span class="accent">邮件</span>系统',
paragraphs: [
'<span class="detail-mid">通过 Postfix 接收邮件,使用 <strong>Pipe to Script</strong> 模式</span>',
'<span class="detail-mid">在 <code class="inline-code">master.cf</code> 注册 Python 脚本,将邮件 JSON 化便于读取</span>',
'<span class="detail-mid">在 <code class="inline-code">/etc/postfix/transport</code> 注册命中规则,让域名下的邮件都走脚本处理</span>',
'<span class="detail-mid">收集各渠道推送 → AI 筛选优先级 → 输出可追溯的<strong>结构化摘要</strong></span>',
],
aside: [
{ kicker: '核心', text: '不是"全都接收",而是<strong>哪些值得看</strong>' },
{ kicker: '可追溯', text: '摘要可回溯原文,不丢信息' },
],
},
},
{
tag: 'System 02',
title: 'RSS',
summary: '可控的信息源',
detail: {
title: '<span class="accent">RSS</span><br>可控的信息入口',
paragraphs: [
'信息源越来越分散 —— 公众号、X、Newsletter、Slack、各种群。需要一个<strong>可控、可订阅、可整理</strong>的入口。',
'RSS 是经过时间验证的答案。<span class="accent-text">订阅制 + AI 摘要</span>,把信息消费从被动推送变回主动选择。',
],
aside: [
{ kicker: '核心', text: '<strong>可控 · 可订阅 · 可整理</strong>。' },
{ kicker: '关键', text: '从被动推送变回主动选择。' },
],
},
},
],
},
{
layout: 'accordion',
chapter: 2,
classes: ['ov-bottom'],
accId: 'acc-6',
overview: {
kicker: '6-2 · 卡片输出 Skill',
title: '展示形式的<br>成本正在<span class="accent">降低</span>',
lead: '从卡片到 HTML、从 Markdown 到 HTML,更直观的表达方式越来越便宜。',
note: '当更好理解的展示形式成本越来越低,人就应该更多使用这种表达。<br>人和 AI 之间的交互不应该永远停留在纯聊天框里。<br><br><a href="https://x.com/trq212/status/2052809885763747935" target="_blank" style="color:var(--accent); font-weight:500; font-size:0.78cqw;">→ 参考原文</a><br><br><span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Skill 01',
title: '卡片总结',
summary: 'HTML 卡片',
detail: {
title: '<span class="accent">卡片</span>总结',
paragraphs: [
'<img src="./part2/卡片总结.webp" class="detail-media">',
],
aside: [
{ kicker: '关键', text: '不只是"好看",而是<strong>更容易理解</strong>' },
{ kicker: '趋势', text: '生成成本越来越低,应该更多使用' },
],
},
},
{
tag: 'Skill 02',
title: '音频早报',
summary: '播客输出',
detail: {
title: '音频<span class="accent">早报</span>',
paragraphs: [
'<img src="./part2/早报.jpg" class="detail-media">',
],
aside: [
{ kicker: '适合', text: '通勤、碎片时间、<strong>被动消费</strong>场景' },
{ kicker: '关键', text: '内容不变,展示形式匹配场景' },
],
},
},
{
tag: 'Skill 03',
title: 'MCP 渲染',
summary: '可交互输出',
detail: {
title: 'MCP <span class="accent">可视化</span>渲染',
paragraphs: [
'<div class="detail-media-row"><img src="./part2/可视化1.png" class="detail-media"><img src="./part2/可视化2.png" class="detail-media"></div>',
],
aside: [
{ kicker: '本质', text: 'AI 输出从<strong>文本</strong>变为<strong>界面</strong>' },
{ kicker: '方向', text: '交互形式还差一点东西,值得探索' },
],
},
},
],
},
{
layout: 'anchor',
chapter: 3,
kicker: 'Part 03 · Harness Engineering',
headline: '用工程化思维<br>驯服 <span class="accent">AI</span>',
subhead: '<strong>Harness 围绕的是如何让一个项目引入了 AI 仍然能够长期开发维护。</strong><br><span style="color: var(--muted);">怎样设计一个让 Agent 可以稳定执行、可以被纠错、可以被验证的工作环境?</span>',
modules: [
{ num: '01', name: '定义方式', tag: '是什么 · 4步阶梯' },
{ num: '02', name: '如何实现', tag: '反盘问 · 意图确认' },
{ num: '03', name: '中途接入', tag: 'CLAUDE.md · Git · Hook' },
{ num: '04', name: '协作编排', tag: '多 Agent · 协作编排' },
],
},
{
layout: 'steps',
chapter: 3,
kicker: '7-1 · Harness 是什么 · 阶梯演进',
headline: '用<span class="accent">文档</span><br>驯服代码',
quote: '技术代码是可抛弃的、可重构的。<br><strong>文档规范是固定的唯一事实依据。</strong>',
steps: [
{ num: '01', title: '需求文档', desc: 'PRD · 目标与边界的源头' },
{ num: '02', title: '架构与系统地图', desc: '<strong>ARCHITECTURE.md</strong> 提供域与包分层的顶层地图,配合 <code class="inline-code">FRONTEND.md</code> / <code class="inline-code">RELIABILITY.md</code> / <code class="inline-code">SECURITY.md</code> / <code class="inline-code">DESIGN.md</code> 等参考文件,让 Agent 直接从代码库推理业务领域' },
{ num: '03', title: '计划与进度日志', desc: '更加具体的执行计划与状态记录,沉淀决策过程' },
{ num: '04', title: '边界与约束', desc: '机械化边界 — <strong>自定义工具与 Linter</strong>;配合<strong>质量追踪与技术债务清单</strong>' },
{ num: '05', title: '精简的 AGENTS.md', desc: '不要塞厚重说明书 — 过多指导让 Agent 针对错误约束优化。提供约 <strong>100 行的 AGENTS.md 作为内容目录和地图</strong>,指引去哪里寻找更深的信息' },
],
},
{
layout: 'accordion',
chapter: 3,
accId: 'acc-8',
// 封面原为 star-history 在线图表(superpowers/trellis/OpenSpec/skills 四仓库 star 增长),
// 该 API 已因 GitHub 限制 star 数据访问而失效,wiki 发布版移除封面、直接展开案例卡片
overview: {
kicker: '7-2 · 如何 Harness · 实践工具',
title: '不是让 AI<br><span class="accent">听话</span>',
lead: '而是建立一套让 AI 高效执行的协作环境。',
note: 'Grill-me 前置确认意图,Skills 规范执行边界,Git 追溯变更。<br><br><span style="color:var(--accent); font-weight:600;">→ 点击此处展开案例</span>',
},
cases: [
{
tag: 'Tool 01',
title: '头脑风暴与烤打',
summary: 'grill-me · 让 AI 给自己找茬',
detail: {
title: '<span class="accent">Grill-me</span> · 烤打我',
paragraphs: [
'<span class="detail-mid">是我用起来<strong>最爽</strong>的一个 skill — 它就是一段话,让 AI 来<strong class="accent-text">给自己找茬</strong>,提出一些可以预见的问题。</span>',
'<blockquote class="detail-quote">Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.<br><br>Ask the questions one at a time.<br><br>If a question can be answered by exploring the codebase, explore the codebase instead.</blockquote>',
'<span class="detail-mid">当你提了一个需求,它会围绕这个需求<strong>不断提出问题</strong>来确认意图,更重要的是提出很多可能会遇到的问题。</span>',
'<span class="detail-mid">这样就预先把很多<span class="accent-text">需要返工、或者开发一半再来问你的问题集中起来</span>,极大避免人必须守在窗口走走停停 — 你的思路也不会被绑架在某个问题上,思考会非常集中。</span>',
'<span class="detail-mid">不管是<strong>大需求</strong>还是<strong>小 demo</strong> 都非常有效。</span>',
],
aside: [
{ kicker: '对比', text: '原来我用 <strong>Plan Mode</strong> 有点像 — 也是和你确认是不是该这么做,但 Plan Mode 还是<strong>太顺从人</strong>了。' },
{ kicker: '核心', text: '让 AI <strong>给自己找茬</strong>,问题前置。' },
{ kicker: '适合', text: '大需求、小 demo 都有效。' },
],
},
},
{
tag: 'Tool 02',
title: 'superpowers',
summary: '',
detail: {},
},
{
tag: 'Tool 03',
title: 'trellis',
summary: '',
detail: {},
},
],
},
{
layout: 'left',
chapter: 4,
kicker: 'Part 04 · 做一个项目吧',
headline: '做完一次完整项目<br><span class="accent">视角才完整</span>',
subhead: '<strong>GAL-Git</strong>|Galgame 叙事 + 多 NPC AI + 云容器<br>把 Git 教学做成职场剧本',
flow: '立项 → 架构 → 玩法剧情 → UI / 美术',
bodyNote: '每一步都做着才发现需要什么',
stats: [
{ num: '2W', note: '行代码' },
{ num: '1+', note: '月业余时间' },
{ num: '已开源', note: 'GitHub' },
],
},
{
layout: 'accordion',
chapter: 4,
classes: ['acc-reversed'],
accId: 'acc-galgit',
coverImg: './part4/gal-git1.jpg',
overview: {
kicker: 'Part 04 · GAL-Git 实机展示',
title: 'Galgame × <span class="accent">Git</span> 教学',
lead: '把枯燥的 Git 命令包进职场剧本,边玩边学。',
note: '<span style="color:var(--accent); font-weight:600;">→ 点击展开查看</span>',
},
cases: [
{
tag: '截图',
title: '界面一览',
summary: '5 张核心界面截图',
detail: {
title: '界面<span class="accent">一览</span>',
paragraphs: [
'<div class="mini-slideshow" data-count="5">'
+ '<div class="ms-track">'
+ '<img src="./part4/gal-git1.jpg" class="ms-slide ms-active" alt="">'
+ '<img src="./part4/gal-git2.jpg" class="ms-slide" alt="">'
+ '<img src="./part4/gal-git3.jpg" class="ms-slide" alt="">'
+ '<img src="./part4/gal-git4.jpg" class="ms-slide" alt="">'
+ '<img src="./part4/gal-git5.jpg" class="ms-slide" alt="">'
+ '</div>'
+ '<div class="ms-nav">'
+ '<button class="ms-prev">‹</button>'
+ '<span class="ms-counter">1 / 5</span>'
+ '<button class="ms-next">›</button>'
+ '</div>'
+ '</div>',
],
},
},
{
tag: '录屏',
title: '演示视频',
summary: '完整游玩流程录屏',
detail: {
title: '演示<span class="accent">视频</span>',
paragraphs: [
'<video src="./part4/录屏2026-05-20 17.19.22.mp4" controls playsinline class="detail-media"></video>',
],
},
},
{
tag: 'LIVE',
title: '在线试玩',
summary: '嵌入在线 Demo',
detail: {
title: '在线<span class="accent">试玩</span>',
paragraphs: [
// 原为公司内网 demo 的 iframe,公开发布版以说明文字替代
'<span class="detail-mid">在线 Demo 当时部署在公司内网,未随本稿公开发布 —— 完整游玩流程见上一张卡片的<strong>演示录屏</strong>。</span>',
],
},
},
{
tag: '副产品',
title: 'Lovave',
summary: '音频版 Lovart · 即听即改',
detail: {
title: '音频版 <span class="accent">Lovart</span>',
paragraphs: [
'<iframe src="https://lovave.liuhetian.work/#proj=test1" class="detail-iframe" allow="clipboard-write; microphone"></iframe>',
],
aside: [
{ kicker: '灵感', text: '开发中发现的<strong>副产品</strong>,即听即改' },
],
},
},
],
},
{
layout: 'hero',
chapter: 5,
classes: ['scene-closing'],
headline: '感谢<span class="accent">聆听</span>',
},
],
};
layouts.js —— 8 种版式的渲染函数
window.Layouts = {
blank() {
return '';
},
hero(s) {
let h = '<div class="col-text">';
if (s.kicker) h += `<div class="kicker">${s.kicker}</div>`;
if (s.headlineMid) h += `<p class="headline-mid">${s.headlineMid}</p>`;
if (s.headline) h += `<h1 class="headline">${s.headline}</h1>`;
if (s.subhead) h += `<p class="subhead">${s.subhead}</p>`;
h += '</div>';
return h;
},
left(s) {
let h = '<div class="col-text">';
if (s.kicker) h += `<div class="kicker">${s.kicker}</div>`;
if (s.headline) h += `<h1 class="headline">${s.headline}</h1>`;
if (s.subhead) h += `<p class="subhead">${s.subhead}</p>`;
if (s.flow) h += `<div class="kicker flow">${s.flow}</div>`;
if (s.bodyNote) h += `<p class="body-note">${s.bodyNote}</p>`;
h += '</div>';
if (s.stats) {
h += '<div class="col-stats">';
s.stats.forEach(st => {
h += '<div class="stat">';
h += `<div class="num">${st.num}</div>`;
h += `<div class="note">${st.note}</div>`;
h += '</div>';
});
h += '</div>';
}
return h;
},
dual(s) {
let h = '';
if (s.kicker) h += `<div class="dual-kicker"><div class="kicker">${s.kicker}</div></div>`;
h += '<div class="dual-cols">';
s.columns.forEach(col => {
h += '<div class="dual-col">';
h += `<div class="dual-col-title">${col.title}</div>`;
h += `<div class="dual-col-body">${col.body}</div>`;
h += '</div>';
});
h += '</div>';
if (s.footer || s.footerNote) {
h += '<div class="dual-footer">';
if (s.footer) h += `<p class="dual-footer-main">${s.footer}</p>`;
if (s.footerNote) h += `<p class="dual-footer-note">${s.footerNote}</p>`;
h += '</div>';
}
return h;
},
anchor(s) {
let h = '<div class="col-text">';
if (s.kicker) h += `<div class="kicker">${s.kicker}</div>`;
if (s.headline) h += `<h1 class="headline">${s.headline}</h1>`;
if (s.subhead) h += `<p class="subhead">${s.subhead}</p>`;
h += '</div>';
if (s.modules) {
h += '<div class="col-stats"><div class="chapter-modules">';
s.modules.forEach(m => {
h += '<div class="mod">';
h += `<div class="mod-num">${m.num}</div>`;
h += `<div class="mod-name">${m.name}</div>`;
h += `<div class="mod-tag">${m.tag}</div>`;
h += '</div>';
});
h += '</div></div>';
}
return h;
},
steps(s) {
let h = '<div class="col-steps">';
if (s.kicker) h += `<div class="kicker">${s.kicker}</div>`;
h += '<div class="steps-list">';
s.steps.forEach(st => {
h += '<div class="step">';
h += `<div class="step-num">${st.num}</div>`;
h += '<div class="step-body">';
h += `<div class="step-title">${st.title}</div>`;
h += `<div class="step-desc">${st.desc}</div>`;
h += '</div></div>';
});
h += '</div></div>';
h += '<div class="col-text">';
if (s.headline) h += `<h1 class="headline">${s.headline}</h1>`;
if (s.quote) h += `<blockquote class="steps-quote">${s.quote}</blockquote>`;
if (s.embed) h += `<div class="steps-embed">${s.embed}</div>`;
h += '</div>';
return h;
},
accordion(s) {
const id = s.accId || 'acc-' + s._idx;
const ov = s.overview;
let h = `<div class="acc" id="${id}">`;
h += '<div class="acc-overview">';
h += `<div class="ov-kicker">${ov.kicker}</div>`;
h += `<h2 class="ov-title">${ov.title}</h2>`;
h += `<p class="ov-lead">${ov.lead}</p>`;
h += `<p class="ov-note">${ov.note}</p>`;
h += '</div>';
h += '<div class="acc-cases-wrap">';
if (s.coverImg) {
h += `<div class="acc-cover">`;
h += `<img src="${s.coverImg}" alt="">`;
h += '<div class="acc-cover-hint">点击查看详情 →</div>';
h += '</div>';
}
h += `<div class="acc-cases${s.coverImg ? ' acc-cases-hidden' : ''}">`;
s.cases.forEach((c, ci) => {
const isEmpty = !c.detail || (!c.detail.title && (!c.detail.paragraphs || !c.detail.paragraphs.length) && (!c.detail.aside || !c.detail.aside.length));
h += `<div class="acc-case${isEmpty ? ' acc-case-empty' : ''}" data-case="${ci + 1}">`;
h += '<div class="case-collapsed">';
h += `<div class="case-tag">${c.tag}</div>`;
h += `<div class="case-title">${c.title}</div>`;
h += `<div class="case-summary">${c.summary}</div>`;
h += '<span class="case-arrow">→</span>';
h += '</div>';
const det = c.detail || {};
h += '<div class="case-detail">';
const hasToggle = det.codeFile || det.reveal;
h += '<div class="detail-head">';
if (hasToggle) {
h += `<div class="detail-title title-toggle" data-toggle="${det.codeFile ? 'code' : 'reveal'}">${det.title || c.title || ''}</div>`;
} else {
h += `<div class="detail-title">${det.title || c.title || ''}</div>`;
}
h += '</div>';
h += '<div class="detail-body">';
h += '<div class="detail-text">';
(det.paragraphs || []).forEach(p => { h += `<p>${p}</p>`; });
h += '</div>';
if (det.codeFile) {
h += '<div class="detail-code-col">';
h += `<div class="detail-code-label">${det.codeFile.split('/').pop()}</div>`;
h += `<pre class="detail-code-block" data-code-src="${det.codeFile}"></pre>`;
h += '</div>';
}
if (det.aside && det.aside.length) {
h += '<div class="detail-aside">';
det.aside.forEach(a => {
h += '<div class="aside-row">';
h += `<div class="aside-kicker">${a.kicker}</div>`;
h += `<div>${a.text}</div>`;
h += '</div>';
});
h += '</div>';
}
h += '</div></div>';
h += '</div>';
});
h += '</div></div></div>';
return h;
},
buildDeck(content, stageInner) {
let html = '';
// bg-layer
html += '<div class="bg-layer">';
content.media.forEach(m => {
if (m.type === 'image') {
html += `<img id="${m.id}" src="${m.src}" alt="">`;
} else {
const attrs = 'muted playsinline preload="auto"' + (m.loop ? ' loop' : '');
html += `<video id="${m.id}" src="${m.src}" ${attrs}></video>`;
}
});
html += '</div>';
// nav
html += '<nav class="nav">';
html += `<div class="nav-logo">${content.meta.logo}</div>`;
html += '<div class="nav-menu">';
content.nav.forEach(n => {
html += `<a href="#" data-jump="${n.jump}">${n.label}</a>`;
});
html += '</div>';
html += '<div class="nav-right">';
html += `<span class="nav-link">${content.meta.version} \xb7 ${content.meta.date}</span>`;
html += '</div></nav>';
// scenes
html += '<div class="scenes">';
content.scenes.forEach((s, i) => {
const extra = (s.classes || []).map(c => ' ' + c).join('');
html += `<section class="scene layout-${s.layout}${extra}" data-scene="${i}" data-chapter="${s.chapter}">`;
s._idx = i;
const renderer = Layouts[s.layout];
if (renderer) html += renderer(s);
html += '</section>';
});
html += '</div>';
// bottom strip
html += '<div class="bottom-strip-anchor">';
html += '<div class="chapter-row" id="chapterRow">';
content.chapters.forEach((ch, i) => {
html += `<span class="ch" data-jump="${i}"><span class="mark ${ch.mark}"></span>${ch.label}</span>`;
});
html += '</div>';
html += `<div class="footer-meta" id="footerMeta">01 / ${String(content.scenes.length).padStart(2, '0')} \xb7 ${content.meta.date}</div>`;
html += '</div>';
// scroll hint + wait corner
html += '<div class="scroll-hint" id="scrollHint">Scroll <span class="bar"></span></div>';
html += '<div class="wait-corner">入场中 \xb7 调试投屏 \xb7 Scroll to begin</div>';
stageInner.innerHTML = html;
},
};
deck.js —— 滚动映射、视频 seek、手风琴与画廊交互
(() => {
const content = window.CONTENT;
const stageInner = document.getElementById('stageInner');
Layouts.buildDeck(content, stageInner);
const SCENE_COUNT = content.scenes.length;
const imgs = content.media.filter(m => m.id.startsWith('s')).map(m => document.getElementById(m.id));
const vidMeta = content.media.filter(m => m.id.startsWith('v'));
const vids = vidMeta.map(m => document.getElementById(m.id));
const transToVidIdx = new Array(SCENE_COUNT - 1).fill(-1);
const vidToTransIdx = [];
vidMeta.forEach((m, vi) => {
const match = m.id.match(/^v(\d+)_/);
if (match) {
const ti = parseInt(match[1], 10);
if (ti < SCENE_COUNT - 1) transToVidIdx[ti] = vi;
vidToTransIdx[vi] = ti;
}
});
const scenes = document.querySelectorAll('.scene');
const hint = document.getElementById('scrollHint');
const footerMeta = document.getElementById('footerMeta');
const chapterRow = document.getElementById('chapterRow');
const navLinks = document.querySelectorAll('.nav-menu a[data-jump]');
const sceneToChapter = content.scenes.map(s => s.chapter);
// ===== scroll mapping =====
const SCENE_WT = 1;
const TRANS_WT_VIDEO = 5;
const TRANS_WT_NONE = 0.3;
const rawSegs = [];
for (let i = 0; i < SCENE_COUNT; i++) {
rawSegs.push({ kind: 'scene', idx: i, w: SCENE_WT });
if (i < SCENE_COUNT - 1) {
rawSegs.push({ kind: 'trans', idx: i, w: transToVidIdx[i] >= 0 ? TRANS_WT_VIDEO : TRANS_WT_NONE });
}
}
const totalW = rawSegs.reduce((s, r) => s + r.w, 0);
const segments = [];
let cursor = 0;
rawSegs.forEach(r => {
const span = r.w / totalW;
segments.push({ start: cursor, end: cursor + span, kind: r.kind, idx: r.idx });
cursor += span;
});
segments[segments.length - 1].end = 1.0;
// looping background videos
const loopBgs = content.media
.filter(m => m.loop)
.map(m => document.getElementById(m.id))
.filter(el => el && el.tagName === 'VIDEO');
loopBgs.forEach(v => v.play().catch(() => {}));
const videoReady = new Array(vids.length).fill(false);
const targetT = new Array(vids.length).fill(0);
vids.forEach((v, i) => {
v.addEventListener('loadedmetadata', () => { videoReady[i] = true; onScroll(); });
});
const lastOpImg = new Array(imgs.length).fill(-1);
const lastOpVid = new Array(vids.length).fill(-1);
let lastActive = -1;
function setImgOp(i, v) {
const r = Math.round(v * 1000) / 1000;
if (lastOpImg[i] !== r) { imgs[i].style.opacity = r; lastOpImg[i] = r; }
if (loopBgs.includes(imgs[i]) && imgs[i].paused && r > 0) imgs[i].play().catch(() => {});
}
function setVidOp(i, v) {
const r = Math.round(v * 1000) / 1000;
if (lastOpVid[i] !== r) { vids[i].style.opacity = r; lastOpVid[i] = r; }
}
function onScroll() {
const max = document.body.scrollHeight - window.innerHeight;
const p = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
const oi = new Array(imgs.length).fill(0);
const ov = new Array(vids.length).fill(0);
let seg = segments[segments.length - 1];
for (let i = 0; i < segments.length; i++) {
if (p < segments[i].end) { seg = segments[i]; break; }
}
let active;
if (seg.kind === 'scene') {
oi[seg.idx] = 1;
active = seg.idx;
} else {
const t = (p - seg.start) / (seg.end - seg.start);
const si = seg.idx;
const vi = transToVidIdx[si];
if (vi >= 0 && videoReady[vi]) {
ov[vi] = 1;
targetT[vi] = t * vids[vi].duration;
} else {
oi[si] = 1 - t;
oi[si + 1] = t;
}
active = (t < 0.5) ? si : (si + 1);
}
for (let i = 0; i < imgs.length; i++) setImgOp(i, oi[i]);
for (let i = 0; i < vids.length; i++) setVidOp(i, ov[i]);
if (active !== lastActive) {
scenes.forEach(sc => sc.classList.toggle('active', +sc.dataset.scene === active));
footerMeta.textContent = String(active + 1).padStart(2, '0') + ' / ' + String(SCENE_COUNT).padStart(2, '0') + ' \xb7 ' + content.meta.date;
const chapIdx = sceneToChapter[active];
chapterRow.querySelectorAll('.ch').forEach((el, i) => el.classList.toggle('active', i === chapIdx));
navLinks.forEach(a => a.classList.toggle('active', sceneToChapter[+a.dataset.jump] === chapIdx));
stageInner.dataset.active = String(active);
if (lastActive !== -1) {
const prevScene = content.scenes[lastActive];
if (prevScene && prevScene.layout === 'accordion') {
closeAllAccordions();
resetAccordionCovers();
}
}
lastActive = active;
}
hint.style.opacity = p > 0.015 ? 0 : 1;
}
function tick() {
for (let i = 0; i < vids.length; i++) {
if (!videoReady[i]) continue;
const v = vids[i];
if (Math.abs(targetT[i] - v.currentTime) > 0.005) v.currentTime = targetT[i];
}
requestAnimationFrame(tick);
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
window.addEventListener('load', onScroll);
// ===== animated scroll =====
const FALLBACK_MS = 500, MIN_MS = 250, MAX_MS = 8000;
function pathTransitionMs(p0, p1) {
const a = Math.min(p0, p1), b = Math.max(p0, p1);
let total = 0, hasVideo = false;
for (let i = 0; i < vids.length; i++) {
if (!videoReady[i]) continue;
const ti = vidToTransIdx[i];
const seg = segments.find(s => s.kind === 'trans' && s.idx === ti);
if (!seg) continue;
const overlap = Math.max(0, Math.min(b, seg.end) - Math.max(a, seg.start));
const span = seg.end - seg.start;
if (overlap > 0 && span > 0) {
total += (overlap / span) * vids[i].duration * 1000;
hasVideo = true;
}
}
if (!hasVideo) return FALLBACK_MS;
return Math.max(MIN_MS, Math.min(MAX_MS, total));
}
let scrollAnim = null;
function scrollToP(targetP, duration) {
const max = document.body.scrollHeight - window.innerHeight;
const target = targetP * max;
const start = window.scrollY;
const delta = target - start;
if (Math.abs(delta) < 1) return;
if (duration === undefined) duration = pathTransitionMs(start / max, targetP);
const t0 = performance.now();
if (scrollAnim) cancelAnimationFrame(scrollAnim);
function step(now) {
const t = Math.min(1, (now - t0) / duration);
window.scrollTo(0, start + delta * t);
if (t < 1) scrollAnim = requestAnimationFrame(step);
else scrollAnim = null;
}
scrollAnim = requestAnimationFrame(step);
}
function gotoScene(idx) {
idx = Math.max(0, Math.min(SCENE_COUNT - 1, idx));
const seg = segments.find(s => s.kind === 'scene' && s.idx === idx);
scrollToP(seg.start + (seg.end - seg.start) * 0.5);
}
function nextScene() { gotoScene((lastActive < 0 ? 0 : lastActive) + 1); }
function prevScene() { gotoScene((lastActive < 0 ? 0 : lastActive) - 1); }
// wire data-jump
document.querySelectorAll('[data-jump]').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault();
gotoScene(+el.dataset.jump);
});
});
// ===== accordion covers =====
document.querySelectorAll('.acc-cover').forEach(cover => {
cover.addEventListener('click', () => {
const cases = cover.parentElement.querySelector('.acc-cases');
cover.classList.add('hidden');
if (cases) cases.classList.add('acc-cases-visible');
});
});
function resetAccordionCovers() {
document.querySelectorAll('.acc-cover').forEach(cover => {
cover.classList.remove('hidden');
const cases = cover.parentElement.querySelector('.acc-cases');
if (cases) cases.classList.remove('acc-cases-visible');
});
}
// ===== accordion =====
const allAccs = document.querySelectorAll('.acc');
function closeAllAccordions() {
allAccs.forEach(acc => {
acc.classList.remove('has-expanded');
acc.querySelectorAll('.acc-case.expanded').forEach(c => c.classList.remove('expanded'));
});
}
function loadCodeBlocks(container) {
container.querySelectorAll('.detail-code-block[data-code-src]').forEach(pre => {
if (pre.dataset.loaded) return;
pre.dataset.loaded = '1';
fetch(pre.dataset.codeSrc)
.then(r => r.ok ? r.text() : Promise.reject())
.then(code => {
pre.textContent = code;
})
.catch(() => {
pre.textContent = '// 无法加载文件';
});
});
}
document.addEventListener('wheel', (e) => {
const block = e.target.closest('.detail-code-block');
if (!block) return;
const atTop = block.scrollTop <= 0 && e.deltaY < 0;
const atBottom = block.scrollTop + block.clientHeight >= block.scrollHeight - 1 && e.deltaY > 0;
if (!atTop && !atBottom) {
e.preventDefault();
e.stopPropagation();
block.scrollTop += e.deltaY;
}
}, { passive: false, capture: true });
document.querySelectorAll('.title-toggle').forEach(title => {
title.addEventListener('click', (e) => {
e.stopPropagation();
const detail = title.closest('.case-detail');
const mode = title.dataset.toggle;
if (mode === 'code') {
detail.classList.toggle('show-code');
if (detail.classList.contains('show-code')) loadCodeBlocks(detail);
} else if (mode === 'reveal') {
detail.querySelectorAll('.reveal-wrap').forEach(r => r.classList.add('revealed'));
}
});
});
allAccs.forEach(acc => {
acc.querySelectorAll('.acc-case').forEach(card => {
card.addEventListener('click', () => {
if (card.classList.contains('acc-case-empty')) return;
if (card.classList.contains('expanded')) {
acc.classList.remove('has-expanded');
acc.querySelectorAll('.acc-case.expanded').forEach(c => {
c.classList.remove('expanded');
const det = c.querySelector('.case-detail');
if (det) det.classList.remove('show-code');
c.querySelectorAll('.reveal-wrap.revealed').forEach(r => r.classList.remove('revealed'));
});
return;
}
acc.querySelectorAll('.acc-case').forEach(c => c.classList.remove('expanded'));
card.classList.add('expanded');
acc.classList.add('has-expanded');
});
});
});
// ===== mini slideshow =====
document.querySelectorAll('.mini-slideshow').forEach(ss => {
const slides = ss.querySelectorAll('.ms-slide');
const counter = ss.querySelector('.ms-counter');
const total = slides.length;
let cur = 0;
const go = (idx) => {
slides[cur].classList.remove('ms-active');
cur = (idx + total) % total;
slides[cur].classList.add('ms-active');
if (counter) counter.textContent = `${cur + 1} / ${total}`;
};
ss.querySelector('.ms-prev')?.addEventListener('click', (e) => { e.stopPropagation(); go(cur - 1); });
ss.querySelector('.ms-next')?.addEventListener('click', (e) => { e.stopPropagation(); go(cur + 1); });
});
// ===== keyboard =====
window.addEventListener('keydown', (e) => {
if (e.target.matches && e.target.matches('input,textarea,[contenteditable]')) return;
if (e.key === 'Escape') {
const anyExpanded = Array.from(allAccs).some(a => a.classList.contains('has-expanded'));
if (anyExpanded) {
e.preventDefault();
allAccs.forEach(acc => {
acc.classList.remove('has-expanded');
acc.querySelectorAll('.acc-case.expanded').forEach(c => c.classList.remove('expanded'));
});
return;
}
}
switch (e.key) {
case 'ArrowRight': case 'PageDown': case ' ':
case 'n': case 'N':
e.preventDefault(); nextScene(); break;
case 'ArrowLeft': case 'PageUp':
case 'p': case 'P':
e.preventDefault(); prevScene(); break;
case 'ArrowDown': e.preventDefault(); nextScene(); break;
case 'ArrowUp': e.preventDefault(); prevScene(); break;
case 'Home': e.preventDefault(); gotoScene(0); break;
case 'End': e.preventDefault(); gotoScene(SCENE_COUNT - 1); break;
case 'f': case 'F':
e.preventDefault();
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
else document.exitFullscreen?.();
break;
default:
if (/^[0-9]$/.test(e.key)) {
e.preventDefault();
const n = parseInt(e.key, 10);
if (n >= 1 && n <= SCENE_COUNT) gotoScene(n - 1);
}
}
});
onScroll();
requestAnimationFrame(tick);
// ===== global timer =====
const timerEl = document.getElementById('globalTimer');
if (timerEl) {
const t0 = Date.now();
setInterval(() => {
const s = Math.floor((Date.now() - t0) / 1000);
const m = Math.floor(s / 60);
const ss = s % 60;
timerEl.textContent = String(m).padStart(2, '0') + ':' + String(ss).padStart(2, '0');
}, 1000);
}
window.Deck = { gotoScene, nextScene, prevScene, closeAccordion: closeAllAccordions };
})();
style.css —— 视觉规范的落地(色彩 token / 字体 / 8 layout / letterbox)
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--ink: #0a0a0a;
--paper: #e8e6e1;
--letterbox: #000;
--accent: #ff6a1a;
--muted: rgba(10, 10, 10, 0.58);
--hairline: rgba(10, 10, 10, 0.18);
--ease: cubic-bezier(0.65, 0, 0.35, 1);
}
html, body {
background: var(--letterbox);
color: var(--ink);
font-family: 'Inter', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
.spacer { height: 1400vh; pointer-events: none; }
/* ============ 16:9 stage ============ */
.stage {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
background: var(--letterbox);
z-index: 0;
}
.stage-inner {
position: relative;
aspect-ratio: 16 / 9;
width: 100vw;
max-width: calc(100vh * 16 / 9);
max-height: 100vh;
overflow: hidden;
background: var(--paper);
container-type: size;
container-name: stage;
}
/* ============ background layers ============ */
.bg-layer { position: absolute; inset: 0; }
.bg-layer img,
.bg-layer video {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.32s ease;
will-change: opacity;
pointer-events: none;
}
/* ============ TOP NAV ============ */
.nav {
position: absolute; top: 0; left: 0; right: 0;
z-index: 30;
display: flex; align-items: center; justify-content: space-between;
padding: 2.6cqh 3cqw;
color: var(--ink);
}
.nav-logo {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 1.35cqw;
letter-spacing: -0.025em;
line-height: 1;
}
.nav-menu {
display: flex; gap: 2.6cqw;
font-size: 0.92cqw;
font-weight: 500;
color: var(--ink);
}
.nav-menu a { color: inherit; text-decoration: none; opacity: 0.85; transition: opacity 0.2s; }
.nav-menu a:hover, .nav-menu a.active { opacity: 1; }
.nav-right { display: flex; align-items: center; gap: 1.2cqw; }
.nav-link {
font-size: 0.92cqw;
font-weight: 500; color: var(--ink); text-decoration: none; opacity: 0.85;
}
/* ============ SCENE base ============ */
.scenes { position: absolute; inset: 0; z-index: 20; pointer-events: none; }
.scene {
position: absolute; inset: 0;
padding: 14cqh 3cqw 10cqh;
display: grid;
column-gap: 3cqw; row-gap: 3cqh;
opacity: 0;
transition: opacity 0.45s ease;
pointer-events: none;
}
.scene.active { opacity: 1; pointer-events: auto; }
.scene .col-text {
display: flex; flex-direction: column;
max-width: 60cqw;
}
.scene .col-stats {
display: flex; flex-direction: column; gap: 4cqh;
max-width: 30cqw;
}
/* ===== layouts ===== */
.scene.layout-left {
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
grid-template-rows: 1fr;
}
.scene.layout-left .col-text { grid-column: 1; grid-row: 1; align-self: start; }
.scene.layout-left .col-stats { grid-column: 2; grid-row: 1; align-self: start; justify-self: end; text-align: right; }
.scene.layout-left .col-stats .stat .note { margin-left: auto; }
.scene.layout-right {
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
grid-template-rows: 1fr;
}
.scene.layout-right .col-text { grid-column: 2; grid-row: 1; align-self: start; justify-self: end; text-align: right; max-width: 56cqw; }
.scene.layout-right .col-text .headline { letter-spacing: -0.02em; }
.scene.layout-right .col-text .subhead { margin-left: auto; max-width: 42cqw; }
.scene.layout-right .col-text .body-note { margin-left: auto; max-width: 38cqw; }
.scene.layout-right .col-text .social { justify-content: flex-end; }
.scene.layout-right .col-stats { grid-column: 1; grid-row: 1; align-self: end; justify-self: start; }
.scene.layout-hero {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
place-items: center;
text-align: center;
padding: 16cqh 4cqw 12cqh;
}
.scene.layout-hero .col-text { grid-column: 1; grid-row: 1; align-items: center; max-width: 78cqw; }
.scene.layout-hero .col-text .headline { font-size: 7cqw; text-align: center; }
.scene.layout-hero .col-text .subhead { text-align: center; max-width: 56cqw; }
.scene.layout-hero .col-text .body-note { text-align: center; max-width: 52cqw; margin-left: auto; margin-right: auto; }
.scene.layout-hero .col-text .social { justify-content: center; }
.scene.layout-hero .col-stats {
grid-column: 1; grid-row: 1;
flex-direction: row; gap: 4cqw;
align-self: end; justify-self: center;
max-width: 78cqw; text-align: center;
}
.scene.layout-hero .col-stats .stat { flex: 1; max-width: 30cqw; }
.scene.layout-hero .col-stats .stat .num { font-size: 1.65cqw; }
.scene.layout-hero .col-stats .stat .note { margin-left: auto; margin-right: auto; }
.scene.layout-anchor {
grid-template-columns: minmax(0, 0.35fr) minmax(0, 0.65fr);
grid-template-rows: 1fr;
padding-top: 13cqh;
align-items: start;
}
.scene.layout-anchor .col-text {
grid-column: 2; grid-row: 1; align-self: start; justify-self: end;
max-width: 58cqw;
text-align: right;
}
.scene.layout-anchor .col-text .headline {
font-size: 8cqw;
line-height: 0.9;
}
.scene.layout-anchor .col-stats {
grid-column: 1; grid-row: 1; align-self: stretch; justify-self: start;
flex-direction: column; gap: 0;
text-align: left;
display: flex; height: 100%;
}
.scene.layout-anchor .col-stats .stat { max-width: 20cqw; }
/* ===== scene-title ===== */
.scene.scene-title {
place-items: start center;
padding-top: 10cqh;
padding-bottom: 50cqh;
}
.scene.scene-title .col-text {
align-self: start;
width: fit-content;
max-width: 88cqw;
display: flex; flex-direction: column;
align-items: stretch;
gap: 0;
}
.scene.scene-title .col-text > * {
margin: 0;
max-width: none;
}
.scene.scene-title .kicker,
.scene.scene-title .subhead {
font-size: 2.36cqw;
font-weight: 400;
letter-spacing: 0;
text-transform: none;
line-height: 1;
margin-bottom: 0;
opacity: 0.7;
text-align: justify;
text-align-last: justify;
}
.scene.scene-title .headline {
font-size: 44.8cqw;
line-height: 1;
text-align: justify;
text-align-last: justify;
}
.scene.scene-title .headline-mid {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 5.9cqw;
line-height: 1;
letter-spacing: 0;
color: var(--ink);
text-align: justify;
text-align-last: justify;
}
/* ===== layout-blank ===== */
.scene.layout-blank {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
place-items: end;
padding: 14cqh 3cqw 10cqh;
}
/* ===== layout-dual ===== */
.scene.layout-dual {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
padding: 13cqh 3cqw 11cqh;
gap: 2.4cqh;
}
.dual-kicker { grid-row: 1; }
.dual-cols {
grid-row: 2;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4cqw;
align-items: start;
}
.dual-col {
display: flex; flex-direction: column;
gap: 2cqh;
}
.dual-col-title {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 5.6cqw;
line-height: 0.94;
letter-spacing: -0.025em;
color: var(--ink);
}
.dual-col-body {
font-size: 1.18cqw;
line-height: 1.65;
color: var(--ink);
font-weight: 400;
}
.dual-col-body .accent { color: var(--accent); }
.dual-footer {
grid-row: 3;
border-top: 1px solid var(--hairline);
padding-top: 1.6cqh;
}
.dual-footer-main {
font-size: 0.92cqw;
font-weight: 500;
color: var(--ink);
line-height: 1.55;
}
.dual-footer-note {
font-size: 0.82cqw;
color: var(--muted);
line-height: 1.55;
margin-top: 0.6cqh;
}
/* ===== scene-closing ===== */
.scene.scene-closing {
place-items: center;
padding: 0;
}
.scene.scene-closing .col-text {
align-items: center;
margin-top: -8cqw;
}
.scene.scene-closing .headline {
font-size: 12cqw;
line-height: 1;
letter-spacing: 0.08em;
text-align: center;
}
/* ===== layout-steps ===== */
.scene.layout-steps {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
align-items: start;
}
.scene.layout-steps .col-steps {
grid-column: 2; grid-row: 1 / 3;
display: flex; flex-direction: column;
align-self: end;
justify-self: end;
width: 100%;
max-width: 56cqw;
}
.scene.layout-steps .col-steps .kicker { margin-bottom: 1.6cqh; }
.scene.layout-steps .steps-list {
display: flex; flex-direction: column;
}
.scene.layout-steps .step {
display: flex;
align-items: baseline;
gap: 1.4cqw;
padding: 1.4cqh 0;
border-bottom: 1px solid var(--hairline);
}
.scene.layout-steps .step:last-child { border-bottom: none; }
.scene.layout-steps .step-num {
font-family: 'Archivo Black', sans-serif;
font-size: 1.4cqw;
letter-spacing: 0.18em;
color: var(--accent);
flex: 0 0 auto;
}
.scene.layout-steps .step-body { flex: 1; min-width: 0; }
.scene.layout-steps .step-title {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 2.2cqw;
line-height: 1.15;
letter-spacing: -0.02em;
color: var(--ink);
}
.scene.layout-steps .step-desc {
font-size: 1.1cqw;
color: var(--muted);
line-height: 1.5;
margin-top: 0.6cqh;
}
.scene.layout-steps .col-text {
grid-column: 1; grid-row: 1;
align-self: start;
justify-self: start;
text-align: left;
max-width: 42cqw;
padding-top: 2cqh;
}
.scene.layout-steps .col-text .headline {
font-size: 6.4cqw;
line-height: 0.92;
}
.scene.layout-steps .steps-quote {
font-size: 1.1cqw;
font-weight: 400;
line-height: 1.65;
color: var(--muted);
margin-top: 3cqh;
max-width: 38cqw;
border: none;
padding: 0;
}
.scene.layout-steps .steps-quote strong {
color: var(--ink);
font-weight: 700;
}
.steps-embed {
margin-top: 2cqh;
width: 100%;
max-width: 50cqw;
margin-left: auto;
}
.steps-embed-img {
width: 100%;
border-radius: 0.6cqh;
display: block;
}
/* ===== layout-accordion ===== */
.scene.layout-accordion {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
padding: 13cqh 3cqw 11cqh;
}
.acc {
grid-column: 1; grid-row: 1;
width: 100%; height: 100%;
display: flex;
gap: 2.4cqw;
overflow: hidden;
}
.acc-overview {
flex: 1 1 50%;
min-width: 0; max-height: 100%;
display: flex; flex-direction: column; justify-content: flex-start;
transition: flex 0.7s var(--ease), opacity 0.45s ease, padding 0.7s var(--ease);
overflow: hidden;
}
.acc.has-expanded .acc-overview {
flex: 0 0 0;
opacity: 0;
padding: 0;
pointer-events: none;
}
.acc-cases {
flex: 1 1 50%;
min-width: 0; min-height: 0; max-height: 100%;
display: flex; flex-direction: row;
gap: 1.2cqw;
transition: flex 0.7s var(--ease), opacity 0.45s ease;
overflow: hidden;
}
.acc.has-expanded .acc-cases { flex: 1 1 100%; }
.acc.has-expanded .acc-cases-wrap { flex: 1 1 100%; }
.acc-case {
flex: 1 1 0;
min-width: 0; min-height: 0; max-height: 100%;
background: rgba(255, 255, 255, 0.68);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--hairline);
border-radius: 1.4cqh;
padding: 2cqh 1.2cqw;
cursor: pointer;
overflow: hidden;
position: relative;
display: flex;
transition: flex 0.7s var(--ease), opacity 0.45s ease, background 0.3s ease, transform 0.3s ease;
}
.acc:not(.has-expanded) .acc-case:hover {
background: rgba(255, 255, 255, 0.88);
flex: 1.35 1 0;
}
.acc-case.acc-case-empty {
cursor: default;
}
.acc:not(.has-expanded) .acc-case.acc-case-empty:hover {
background: rgba(255, 255, 255, 0.68);
flex: 1 1 0;
}
.acc.has-expanded .acc-case { flex: 0 0 0; opacity: 0; padding-left: 0; padding-right: 0; }
.acc.has-expanded .acc-case.expanded {
flex: 1 1 100%;
opacity: 1;
padding: 2.5cqh 3cqw;
max-height: 100%;
background: rgba(255, 255, 255, 0.92);
transform: none;
cursor: zoom-out;
overflow: hidden;
}
.acc-case .case-collapsed {
display: flex; flex-direction: column;
justify-content: space-between; align-items: flex-start;
width: 100%; height: 100%;
min-height: 0;
}
.acc-case .case-tag {
font-family: 'Inter', 'Noto Sans SC', sans-serif;
font-size: 0.78cqw;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
flex: 0 0 auto;
white-space: nowrap;
}
.acc-case .case-title {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 2.4cqw;
line-height: 1.05;
letter-spacing: 0.08em;
color: var(--ink);
writing-mode: vertical-rl;
text-orientation: upright;
flex: 1 1 auto;
min-height: 0;
margin: 1.4cqh 0 1.4cqh 0.2cqw;
align-self: center;
}
.acc-case .case-summary { display: none; }
.acc-case .case-arrow {
width: 3cqh; height: 3cqh; min-width: 1.5rem; min-height: 1.5rem;
border-radius: 50%;
background: var(--ink); color: #fff;
display: inline-flex; align-items: center; justify-content: center;
font-size: 1cqw;
font-weight: 700;
flex: 0 0 auto;
align-self: flex-start;
transition: transform 0.3s var(--ease);
}
.acc:not(.has-expanded) .acc-case:hover .case-arrow {
transform: translateX(-0.4cqw);
}
.acc-case .case-detail {
opacity: 0;
display: none;
transition: opacity 0.4s ease 0.25s;
}
.acc.has-expanded .acc-case.expanded .case-detail {
opacity: 1;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
min-height: 0;
position: relative;
}
.acc.has-expanded .acc-case.expanded .case-collapsed { display: none; }
.case-detail .detail-head {
margin-bottom: 1.2cqh;
flex: 0 0 auto;
}
.title-toggle {
cursor: pointer;
transition: opacity 0.2s;
}
.title-toggle:hover {
opacity: 0.65;
}
.case-detail .detail-title {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 3cqw;
font-weight: 900;
line-height: 1;
letter-spacing: -0.025em;
}
.case-detail .detail-title .accent { color: var(--accent); }
.case-detail .detail-body {
display: flex;
flex-direction: column;
gap: 0;
flex: 1; min-height: 0;
overflow: hidden;
}
.case-detail .detail-body .detail-text {
font-size: 1.1cqw;
line-height: 1.55;
color: var(--ink);
overflow: hidden;
flex: 1; min-height: 0;
display: flex;
flex-direction: column;
gap: 0.8cqh;
}
.case-detail .detail-code-col {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
min-height: 0;
background: rgba(30, 30, 46, 0.97);
border-radius: 1cqh;
padding: 0.8cqh 0.8cqw;
opacity: 0;
pointer-events: none;
transform: translateX(3%);
transition: opacity 0.35s ease, transform 0.35s ease;
z-index: 5;
}
.case-detail.show-code .detail-code-col {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
.case-detail .detail-body .detail-text p { margin-bottom: 0; flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; width: 100%; }
.case-detail .detail-body .detail-text strong { font-weight: 700; }
.case-detail .detail-body .detail-text .accent-text { color: var(--accent); font-weight: 600; }
.case-detail .detail-aside {
border-top: 1px solid var(--hairline);
padding-top: 1.2cqh;
display: flex; flex-direction: row; flex-wrap: wrap; gap: 1cqh 3cqw;
font-size: 0.92cqw;
color: var(--muted);
line-height: 1.5;
overflow: hidden;
flex: 0 0 auto;
}
.case-detail .detail-aside:empty {
display: none;
}
.case-detail .detail-aside .aside-kicker {
font-family: 'Inter', sans-serif;
font-size: 0.78cqw;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink);
opacity: 0.7;
font-weight: 500;
}
.case-detail .detail-aside .aside-row {
display: flex; flex-direction: column; gap: 0.4cqh;
}
.case-detail .detail-aside .aside-row strong {
color: var(--ink); font-weight: 600;
}
/* overview pinned to bottom (for scenes where dog is in top-left) */
.scene.ov-bottom .acc-overview {
justify-content: flex-end;
}
/* reversed accordion: overview on right, cases on left */
.scene.acc-reversed .acc {
flex-direction: row-reverse;
}
/* blank placeholder for unexplored cases */
.blank-placeholder {
display: block;
text-align: center;
font-family: 'Archivo Black', sans-serif;
font-size: 10cqw;
font-weight: 900;
line-height: 1;
color: var(--hairline);
margin-top: 2cqh;
user-select: none;
}
/* accordion cover over cases area */
.acc-cases-wrap {
position: relative;
flex: 1 1 50%;
min-width: 0; min-height: 0; max-height: 100%;
overflow: hidden;
}
.acc-cases-wrap .acc-cases {
flex: none;
width: 100%; height: 100%;
display: flex; flex-direction: row;
gap: 1.2cqw;
overflow: hidden;
}
.acc-cover {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: pointer;
z-index: 3;
transition: opacity 0.45s ease;
}
.acc-cover img {
max-width: 100%;
max-height: 78%;
object-fit: contain;
border-radius: 1cqh;
transition: transform 0.3s ease;
}
.acc-cover:hover img {
transform: scale(1.01);
}
.acc-cover-hint {
margin-top: 1.4cqh;
font-size: 0.92cqw;
font-weight: 600;
color: var(--accent);
opacity: 0.7;
transition: opacity 0.3s ease;
}
.acc-cover:hover .acc-cover-hint { opacity: 1; }
.acc-cover.hidden {
opacity: 0;
pointer-events: none;
}
.acc-cases-hidden {
opacity: 0;
pointer-events: none;
transition: opacity 0.45s ease;
}
.acc-cases-hidden.acc-cases-visible {
opacity: 1;
pointer-events: auto;
}
/* embedded media in detail */
.detail-media {
max-width: 100%;
height: 100%;
border-radius: 0.6cqh;
margin: 0;
object-fit: contain;
display: block;
}
.detail-media-row {
display: flex;
gap: 1.2cqw;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.detail-media-row .detail-media {
flex: 1 1 0;
min-width: 0;
max-width: 50%;
height: 100%;
object-fit: contain;
}
/* ===== mini slideshow ===== */
.mini-slideshow {
width: 100%; height: 100%;
display: flex; flex-direction: column;
position: relative;
overflow: hidden;
}
.ms-track {
flex: 1; min-height: 0;
position: relative;
}
.ms-slide {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: contain;
border-radius: 0.6cqh;
opacity: 0;
transition: opacity 0.35s ease;
pointer-events: none;
}
.ms-slide.ms-active {
opacity: 1;
pointer-events: auto;
}
.ms-nav {
flex: 0 0 auto;
display: flex; align-items: center; justify-content: center;
gap: 1.5cqw;
padding: 0.6cqh 0;
}
.ms-prev, .ms-next {
background: none; border: 1px solid var(--hairline);
border-radius: 50%;
width: 2.4cqw; height: 2.4cqw;
font-size: 1.4cqw; line-height: 1;
cursor: pointer;
color: var(--ink);
display: flex; align-items: center; justify-content: center;
transition: background 0.2s, border-color 0.2s;
}
.ms-prev:hover, .ms-next:hover {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.ms-counter {
font-size: 0.9cqw;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ===== detail iframe ===== */
.detail-iframe {
width: 100%; height: 100%;
border: none;
border-radius: 0.6cqh;
background: #fff;
}
/* reveal button + hidden image */
.reveal-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
}
.reveal-wrap .reveal-img {
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.reveal-wrap.revealed .reveal-img {
opacity: 1;
pointer-events: auto;
}
.detail-code-block {
background: #1e1e2e;
color: #cdd6f4;
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
font-size: 0.78cqw;
line-height: 1.55;
border-radius: 0.6cqh;
padding: 1.2cqh 1.2cqw;
margin: 0;
overflow-y: auto;
overflow-x: auto;
flex: 1;
min-height: 0;
white-space: pre;
tab-size: 2;
-moz-tab-size: 2;
}
.detail-code-block::-webkit-scrollbar {
width: 6px; height: 6px;
}
.detail-code-block::-webkit-scrollbar-track {
background: transparent;
}
.detail-code-block::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.18);
border-radius: 3px;
}
.detail-code-block::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.3);
}
.detail-code-block .hl-kw { color: #cba6f7; }
.detail-code-block .hl-str { color: #a6e3a1; }
.detail-code-block .hl-cm { color: #6c7086; font-style: italic; }
.detail-code-block .hl-fn { color: #89b4fa; }
.detail-code-block .hl-num { color: #fab387; }
.detail-code-label {
font-family: 'Inter', sans-serif;
font-size: 0.72cqw;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #a6adc8;
display: inline-block;
padding: 0.3cqh 0;
flex: 0 0 auto;
}
.aside-img {
max-width: 100%;
max-height: 16cqh;
border-radius: 0.4cqh;
margin: 0.4cqh 0;
object-fit: contain;
display: block;
}
.ov-img {
max-width: 100%;
max-height: 22cqh;
border-radius: 0.8cqh;
margin-top: 1.2cqh;
object-fit: contain;
display: block;
}
.detail-code {
display: block;
background: rgba(10,10,10,0.06);
border-radius: 0.6cqh;
padding: 1.2cqh 1.4cqw;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.72cqw;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
color: var(--ink);
overflow: auto;
max-height: 30cqh;
}
.detail-link {
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 0;
min-height: 0;
color: var(--ink);
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 3cqw;
letter-spacing: -0.02em;
text-decoration: none;
background: none;
border: none;
transition: color 0.25s;
}
.detail-link:hover {
color: var(--accent);
}
/* detail text sizes */
.detail-big {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 2.4cqw;
font-weight: 900;
line-height: 1.15;
color: var(--ink);
}
.detail-mid {
font-size: 1.2cqw;
font-weight: 500;
line-height: 1.65;
color: var(--ink);
}
.detail-mid .accent-text { color: var(--accent); font-weight: 700; }
.detail-quote {
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.92cqw;
line-height: 1.6;
color: var(--ink);
background: rgba(0,0,0,0.04);
border-left: 3px solid var(--accent);
padding: 1.2cqh 1.4cqw;
margin: 0;
border-radius: 0 0.4cqh 0.4cqh 0;
}
.inline-code {
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.88em;
background: rgba(0,0,0,0.06);
padding: 0.15em 0.4em;
border-radius: 0.3em;
}
.click-reveal {
cursor: pointer;
position: relative;
}
.click-reveal img { display: none; }
.click-reveal .click-reveal-hint {
display: inline-block;
color: var(--accent);
font-weight: 600;
font-size: 0.92cqw;
padding: 1cqh 0;
}
.click-reveal.revealed img { display: block; }
.click-reveal.revealed .click-reveal-hint { display: none; }
/* overview content styles */
.acc-overview .ov-kicker {
font-family: 'Inter', 'Noto Sans SC', sans-serif;
font-size: 0.82cqw;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink);
margin-bottom: 1.6cqh;
opacity: 0.7;
font-weight: 500;
}
.acc-overview .ov-title {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 5cqw;
line-height: 0.94;
letter-spacing: -0.025em;
margin-bottom: 2.4cqh;
color: var(--ink);
}
.acc-overview .ov-title .accent { color: var(--accent); }
.acc-overview .ov-lead {
font-size: 1.18cqw;
font-weight: 500;
line-height: 1.55;
color: var(--ink);
margin-bottom: 2.4cqh;
max-width: 36cqw;
}
.acc-overview .ov-note {
font-size: 0.92cqw;
color: var(--muted);
line-height: 1.65;
max-width: 34cqw;
}
.acc-overview .ov-note strong { color: var(--ink); font-weight: 600; }
/* ============ chapter modules grid ============ */
.chapter-modules {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
margin-top: 0;
}
.chapter-modules .mod {
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
border-radius: 0;
padding: 1cqh 0;
text-align: left;
border-bottom: 1px solid var(--hairline);
}
.chapter-modules .mod:last-child {
border-bottom: none;
}
.chapter-modules .mod .mod-num {
font-family: 'Archivo Black', sans-serif;
font-size: 0.88cqw;
letter-spacing: 0.18em;
color: var(--accent);
margin-bottom: 0.4cqh;
}
.chapter-modules .mod .mod-name {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 5.6cqw;
font-weight: 900;
line-height: 0.95;
letter-spacing: -0.03em;
white-space: nowrap;
}
.chapter-modules .mod .mod-tag {
font-size: 0.84cqw;
color: var(--muted);
margin-top: 0.4cqh;
line-height: 1.5;
}
/* ============ typography ============ */
.kicker {
font-family: 'Inter', 'Noto Sans SC', sans-serif;
font-size: 0.82cqw;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink);
margin-bottom: 1.2cqh;
opacity: 0.7;
font-weight: 500;
}
.headline {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-weight: 900;
font-size: 5.6cqw;
line-height: 0.94;
letter-spacing: -0.025em;
margin-bottom: 2.4cqh;
color: var(--ink);
}
.headline .en { font-family: 'Archivo Black', sans-serif; text-transform: uppercase; }
.headline .accent { color: var(--accent); }
.subhead {
font-size: 1.18cqw;
font-weight: 400;
line-height: 1.55;
color: var(--ink);
margin-bottom: 3cqh;
max-width: 42cqw;
}
.subhead strong { font-weight: 700; }
.subhead .accent-text { color: var(--accent); font-weight: 700; }
.body-note {
font-size: 0.92cqw;
font-weight: 400;
line-height: 1.65;
color: var(--ink);
max-width: 38cqw;
margin-bottom: 1.8cqh;
}
.body-note strong { font-weight: 700; }
.body-note .accent-text { color: var(--accent); font-weight: 600; }
.social {
display: flex; gap: 1.2cqw;
font-size: 0.88cqw;
font-weight: 500;
color: var(--ink);
margin-top: auto;
padding-top: 2cqh;
}
.social a { color: inherit; text-decoration: none; opacity: 0.7; transition: opacity 0.2s; pointer-events: auto; }
.social a:hover { opacity: 1; }
.stat .num {
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 1.85cqw;
line-height: 1.05;
letter-spacing: -0.015em;
text-transform: uppercase;
margin-bottom: 1cqh;
color: var(--ink);
}
.stat .num .accent { color: var(--accent); }
.stat .note {
font-size: 0.88cqw;
color: var(--muted);
line-height: 1.55;
max-width: 26cqw;
font-weight: 400;
}
.stat .note strong { color: var(--ink); font-weight: 600; }
/* ============ BOTTOM strip ============ */
.bottom-strip-anchor {
position: absolute;
left: 3cqw; right: 3cqw; bottom: 4cqh;
z-index: 25;
display: flex; align-items: center; justify-content: space-between;
gap: 1.5cqw;
border-top: 1px solid rgba(10,10,10,0.14);
padding-top: 2cqh;
transition: opacity 0.4s ease;
}
.chapter-row {
display: flex; align-items: center; gap: 1.5cqw;
flex-wrap: wrap;
font-family: 'Archivo Black', 'Noto Sans SC', sans-serif;
font-size: 0.92cqw;
letter-spacing: 0.02em;
color: var(--ink);
}
.chapter-row .ch {
display: inline-flex; align-items: center; gap: 0.5cqw;
opacity: 0.32; transition: opacity 0.3s ease;
cursor: pointer; pointer-events: auto;
}
.chapter-row .ch.active { opacity: 1; color: var(--accent); }
.chapter-row .ch .mark {
width: 0.85cqw; height: 0.85cqw; min-width: 9px; min-height: 9px;
display: inline-block;
background: currentColor;
}
.chapter-row .ch .mark.circle { border-radius: 50%; }
.chapter-row .ch .mark.square { border-radius: 1px; }
.chapter-row .ch .mark.tri {
background: transparent;
border-left: 0.42cqw solid transparent;
border-right: 0.42cqw solid transparent;
border-bottom: 0.72cqw solid currentColor;
}
.chapter-row .ch .mark.bar { height: 0.32cqw; min-height: 4px; border-radius: 1px; }
.chapter-row .ch .mark.dot { border-radius: 50%; width: 0.5cqw; height: 0.5cqw; min-width: 5px; min-height: 5px; }
.footer-meta {
font-family: 'Inter', monospace;
font-size: 0.82cqw;
color: var(--muted);
letter-spacing: 0.16em;
text-transform: uppercase;
font-weight: 500;
}
.stage-inner[data-active="0"] .bottom-strip-anchor { opacity: 0; pointer-events: none; }
/* ============ scroll hint ============ */
.scroll-hint {
position: absolute;
bottom: 3cqh; left: 50%;
transform: translateX(-50%);
z-index: 40;
font-family: 'Inter', monospace;
font-size: 0.78cqw;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--ink);
opacity: 1;
transition: opacity 0.4s ease;
display: flex; align-items: center; gap: 0.6rem;
pointer-events: none; font-weight: 600;
}
.scroll-hint .bar {
display: inline-block; width: 1px; height: 1.3rem;
background: currentColor;
animation: drop 1.6s ease-in-out infinite;
transform-origin: top;
}
@keyframes drop {
0% { transform: scaleY(0); transform-origin: top; }
50% { transform: scaleY(1); transform-origin: top; }
51% { transform: scaleY(1); transform-origin: bottom; }
100% { transform: scaleY(0); transform-origin: bottom; }
}
.wait-corner {
position: absolute;
bottom: 4cqh; right: 3cqw;
z-index: 35;
font-family: 'Inter', monospace;
font-size: 0.82cqw;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--ink);
opacity: 0;
transition: opacity 0.4s ease;
font-weight: 500;
}
.stage-inner[data-active="0"] .wait-corner { opacity: 0.55; }
/* ===== global timer ===== */
.global-timer {
position: fixed;
bottom: 1.2vh;
left: 1.2vw;
font-family: 'Inter', monospace;
font-size: 1.1vw;
font-variant-numeric: tabular-nums;
color: rgba(255, 255, 255, 0.45);
z-index: 9999;
pointer-events: none;
letter-spacing: 0.05em;
}
@media (max-width: 720px) {
.nav-menu { display: none; }
.nav-link { display: none; }
.chapter-row .ch:nth-child(n+5) { display: none; }
}
制作化石一份,存档自原项目目录(获取日期 2026-07-07):
assets/prompt-v0.md —— 初版滚动开场的一次成型 prompt(原文写于 2026-05-16)
# 滚动叙事开场 — tap4fun 公司简介
做一个**滚动驱动**的单页 PPT 开场。用户向下滚动,背景在 3 个静帧场景之间用 2 段过渡视频无缝衔接,前景文案随场景切换。**不循环**,滚到底停在最后一帧。
---
## 一、资产清单
当前目录文件(路径用相对路径,**已去除空格**):
```
./scene1.png 场景1静帧(开场)
./scene2.png 场景2静帧(中段)
./scene3.png 场景3静帧(终场)
./trans1to2.mp4 场景1→2 过渡视频
./trans2to3.mp4 场景2→3 过渡视频
./reference.png 排版参考图(仅用于学习版式,不出现在页面里)
```
**重要**:没有一条贯穿始终的 main 视频。叙事是 "静帧 → 过渡视频 → 静帧 → 过渡视频 → 静帧" 的 5 段混合结构。
---
## 二、叙事结构与滚动映射
页面总高度 = `500vh`。把全局 scroll 进度 `p ∈ [0, 1]` 划成 5 个区间:
| 区间 | p 范围 | 背景层 | 文案 |
|---|---|---|---|
| ① | 0.00 – 0.20 | `scene1.png` 静帧 | 场景1文案常显 |
| ② | 0.20 – 0.40 | `trans1to2.mp4` 播放 | 文案 fade 1→2 |
| ③ | 0.40 – 0.60 | `scene2.png` 静帧 | 场景2文案常显 |
| ④ | 0.60 – 0.80 | `trans2to3.mp4` 播放 | 文案 fade 2→3 |
| ⑤ | 0.80 – 1.00 | `scene3.png` 静帧 | 场景3文案 + CTA |
视频驱动规则:
- 在区间 ② 内:`trans1to2.currentTime = ((p - 0.20) / 0.20) × trans1to2.duration`
- 在区间 ④ 内:`trans2to3.currentTime = ((p - 0.60) / 0.20) × trans2to3.duration`
- 静帧区间:对应图片 `opacity:1`,所有视频 `opacity:0`
- 过渡区间:对应视频 `opacity:1`,所有图片 `opacity:0`
- 层切换用 `0.3s ease` opacity 过渡,避免硬切
**不循环**。滚动到底停在场景3,CTA 可点击。
---
## 三、文案内容(tap4fun 公司简介)
所有文案沿用 reference.png 的版式骨架:**顶部导航 + 左上大标题/副标/CTA + 右上两组统计 + 底部品牌行 + 社交图标**。
顶部导航(三个场景共享,固定):
- 左:`tap4fun` logo
- 中:About / Games / Studio / Careers / Contact
- 右:`Press Kit`(次按钮)/ `Join Us`(主按钮)
### 场景 1 — Brand Vision
- 主标题:`BUILDING WORLDS THAT ENDURE`
- 副标:Global mobile strategy, crafted in Chengdu since 2008.
- 描述(CTA 下方小字):A studio of builders, designers, and engineers shaping interactive worlds that millions return to.
- CTA pill:`Discover tap4fun →`
- 右上统计 ×2:
- `200M+ PLAYERS WORLDWIDE` — From day-one launches to franchises that still grow today.
- `15+ YEARS IN THE INDUSTRY` — Pioneers of cross-cultural SLG, scaling across regions.
### 场景 2 — Portfolio
- 主标题:`GAMES THAT DEFINE A GENRE`
- 副标:Strategy titles that turn empires into stories.
- 描述:From real-time war to long-tail kingdoms — operated live, every hour, every region.
- CTA pill:`Explore our games →`
- 右上统计 ×2:
- `10+ FLAGSHIP TITLES` — Invasion: Modern Empire · Last Empire–War Z · Age of Z Origins.
- `150+ COUNTRIES SERVED` — 20+ languages, 24/7 live operations.
### 场景 3 — Team & Future
- 主标题:`THE NEXT WORLD STARTS HERE`
- 副标:Builders, designers, engineers — one studio, one mission.
- 描述:We are hiring across engineering, art, game design, and live operations.
- CTA pill:`Join the studio →`
- 右上统计 ×2:
- `800+ TEAM MEMBERS` — Engineering, art, ops, and live-game craft, under one roof.
- `∞ POSSIBILITIES` — AI, cross-platform, new frontiers in interactive entertainment.
- 底部品牌行(只在场景3显示):`Invasion · Last Empire · Age of Z · Kings Throne · Hero Clash`
- 社交图标:Facebook · X · LinkedIn · Bilibili
---
## 四、排版语言(学习 reference.png)
观察 reference.png 后还原以下要点:
- **字体**
- 主标题:粗体无衬线 condensed/grotesk,全大写,字重对比极强 → 用 Google Fonts `Archivo Black` 或 `Anton`
- 正文/副标:现代无衬线 → `Inter` 400/500
- 统计数字:与主标同字体保持一致性
- **字号层级**:主标 `clamp(2.8rem, 6vw, 5.5rem)` / 统计数字 `clamp(1.8rem, 3vw, 2.6rem)` / 副标 `1rem` / 描述 `0.85rem`
- **对齐**:非对称三栏——左上 / 右上 / 底部一行;导航横贯顶部
- **颜色**:背景由场景图决定,文字默认黑色 `#0a0a0a`;CTA pill 黑底白字带白色圆形 → 箭头 icon;强调色橙 `#FF6A1A`(用在 hover / 强调小元素)
- **装饰**:CTA 是 pill button 带末尾圆形 → 图标;统计数字下有 1 行细描述;底部品牌名用 `·` 分隔
- **气质**:极简、留白大、字重对比强、设计克制
- **关键原则**:**三个场景共用同一套版式骨架**,只换文字内容。滚动时让用户感到"画面在变、版式在呼吸",而不是切到一个完全不同的页面。
---
## 五、技术实现
技术栈:**纯 HTML + CSS + JavaScript 单文件**,不用任何框架。直接通过 Google Fonts CDN 引字体。
### 背景层结构
```html
<div class="bg-layer">
<img id="s1" src="./scene1.png">
<video id="v12" src="./trans1to2.mp4" muted playsinline preload="auto"></video>
<img id="s2" src="./scene2.png">
<video id="v23" src="./trans2to3.mp4" muted playsinline preload="auto"></video>
<img id="s3" src="./scene3.png">
</div>
```
所有 5 个元素:`position: fixed; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: opacity 0.3s ease;`
### 视频要点(避坑)
- `muted` + `playsinline` + `preload="auto"` 必须有
- **不要** `autoplay`,**不要** `controls`,**不要** `loop`
- iOS Safari 必须 `playsinline`(小写)
- 视频用 `currentTime` 驱动,不调用 `play()`
### 滚动驱动
```js
let targetT12 = 0, targetT23 = 0;
function onScroll() {
const p = window.scrollY / (document.body.scrollHeight - window.innerHeight);
// 计算各层 opacity 与视频 currentTime
// 用变量缓存目标值,rAF 里再赋给 video.currentTime
}
function tick() {
v12.currentTime += (targetT12 - v12.currentTime) * 0.25; // 缓动 seek
v23.currentTime += (targetT23 - v23.currentTime) * 0.25;
requestAnimationFrame(tick);
}
window.addEventListener('scroll', onScroll, { passive: true });
requestAnimationFrame(tick);
```
关键:scroll 事件只更新目标值,真正的 `currentTime` 赋值放在 rAF 里做缓动,避免连续 seek 卡顿。
### 文案层
- 三段文案各自 `position: fixed`,按 reference.png 三栏骨架布局
- opacity 由所在区间决定,`transition: opacity 0.4s ease`
- 同时只有一段处于 `opacity:1`
### 响应式
- 断点 `≤768px`:左右两栏堆叠为单列;导航折叠为汉堡(或简化为只显示 logo + Join Us);主标题用 `clamp()` 自适应
- 底部品牌行在移动端换行展示,分隔点保留
### 其他
- 页面初始 `body { overflow-x: hidden }`,纵向 scroll height 由一个 `<div style="height:500vh"></div>` spacer 撑开
- 字体预加载,避免 FOUT
- 不需要做加载进度条,但首屏前确保 `scene1.png` 已 decode(用 `<link rel="preload">` 或 `img.decode()`)
---
## 六、输出
**直接输出完整 `index.html` 单文件**(含内联 CSS 与 JS,引用 Google Fonts CDN)。资产用相对路径引用,文件命名见【一】。不要输出说明文字,只要 HTML。