Interactive Course
深入 970+ 行 HTML、CSS 和 JavaScript,看看 201 个词是怎么变成一个有轮播、搜索、动画和游戏化的交互式页面的。
从数据到动画
小测验 & 实时演示
全程大白话解释
Scroll down to begin ↓
这个应用是干嘛的?201 个词是从哪来的?
当 Claude Code 在思考的时候,会显示一个小转圈加一个随机词。不是无聊的"思考中..."——而是会蹦出各种好玩的词,比如 Simmering(文火慢炖)、Moonwalking(太空步)、或者 Discombobulating(迷惑中)。
这些词是硬编码在 Claude Code 的源文件里的——一个巨大的 JavaScript 文件叫 cli.js。
有人(ππ!)用 Python 和正则表达式把所有转圈词从压缩代码里挖了出来(最新版 v2.1.79 有 191 个)。还有 8 个过去式的"完成词"(比如 Crunched),是思考结束后显示的。另外还有 2 个已退役的词(Sautéing 和 Flambéing),总共 201 个。
cli.js 在 node_modules/@anthropic-ai/claude-code/ 目录里
用 Grep 搜 "Thinking" 或 "Cooking",定位到数组的位置
把周围所有大写开头的 "-ing" 词全部提取出来
分成 10 个类别,加上 emoji、中文翻译和趣味解释,做成可交互的页面
成品是一个单独的 HTML 文件——大约 970 行代码——打包了轮播、搜索、转圈动画、趣味解释弹窗和收集游戏。不需要外部库,不需要构建工具,不需要服务器。打开文件就能玩。
单个自包含的 HTML 文件意味着零配置。你可以发邮件分享、放到 GitHub Pages 上、或者离线打开。这个限制反而逼出了精炼高效的代码——每一行都有存在的意义。
四个对象撑起了整个页面所需的全部信息
整个词汇库存放在四个 JavaScript 对象里。你可以把它们想象成四个文件柜,每个柜子存着同一批词的不同信息。
给定一个词比如"Cooking",应该配什么 emoji?E 说:🍳
给定"Cooking",中文怎么说?Z 说:烹饪中
词怎么分组?D 定义了 10 个类别,包括图标、颜色和精选词汇。
"Ruminating"为什么配牛的emoji?F 给你一句话解答:🐄 像牛反刍一样把问题嚼了又嚼
const E = {
'Thinking': '🤔',
'Cooking': '🍳',
'Clauding': '🤖',
// ... 还有 198 条
};
创建一个叫 E 的永久查找表。
有人问"Thinking 配什么 emoji?"——答案是:🤔
Cooking 呢?🍳
Clauding(Claude 用自己名字造的词!)配机器人脸 🤖
每个词都按这个模式一一对应。
D 里的每个类别是一个对象,有固定的结构:
{
icon: '🧠',
th: 'purple',
en: 'Serious Thinking',
zh: '正经思考',
f: ['Thinking', 'Pondering', ...],
w: ['Thinking', 'Pondering', ...]
}
icon:标签栏显示的 emoji(🧠 大脑代表思考类)
th:主题名——决定背景的浅色调
en/zh:类别的英文名和中文名
f:"精选"词——页面上大卡片展示的 8 个词
w:这个类别的全部词(包含精选的)
页面只突出展示 8 个"精选"词。其他词藏在"显示全部"按钮后面。这叫渐进式披露——先展示简单的,需要时再展开复杂的。
当你点击一个词卡时,三个数据对象会联手合作。来看看它们的"群聊":
10 张幻灯片如何用一行 CSS 左右滑动
想象一条长长的胶片横着摆放,每一帧刚好是屏幕的宽度。一个窗口(视口)一次只显示一帧。要切换画面,只需要把整条胶片往左或往右拉。
这个轮播就是这么工作的。所有 10 张幻灯片在一个 Flexbox 行里并排放着。因为容器隐藏了溢出的部分,所以一次只能看到一张。
.slides-area {
flex: 1;
overflow: hidden;
}
.slides-track {
display: flex;
transition: transform .45s;
}
.slide {
min-width: 100%;
}
观察窗口占满可用空间,超出边缘的部分全部隐藏。
里面的所有幻灯片排成一横行。移动这一行时,用 0.45 秒的动画平滑过渡。
每张幻灯片的宽度正好和窗口一样——所以一次只显示一张。
切换幻灯片简单得令人惊讶。一行代码完成所有重活:
function go(i) {
cur = Math.max(0, Math.min(i, D.length-1));
track.style.transform =
`translateX(-${cur*100}%)`;
}
定义一个叫"go"的函数,接收一个幻灯片编号。
把编号限制在 0 到最后一张之间——这样就不会滑出边界。
把整条轨道向左移动(编号 × 100)%。第 0 张 = 0%,第 1 张 = -100%,第 2 张 = -200%,以此类推。
用 transform: translateX() 而不是改 left 是一个性能技巧。transform 由 GPU(显卡)处理,即使在老设备上动画也很流畅。
轮播同时支持按钮点击、键盘操作和触屏滑动。三种方式都调用同一个 go() 函数:
onclick=go(cur-1) 和 onclick=go(cur+1)。简单的点击处理。
监听左右方向键。同样的函数:go(cur-1) 或 go(cur+1)。
在 touchstart 记录手指位置,touchend 时比较。如果滑动距离 >50px,就切换幻灯片。
转圈动画如何制造出"哒哒哒...停!"的满足感
点"随机转一个",词就会飞速闪过,然后逐渐慢下来,直到最后一个词"停住"。感觉就像老虎机——代码的工作原理也很类似。
核心技巧:一个递归函数用越来越长的间隔调用自己。开始时很快(60毫秒),后面逐渐变慢(最多310毫秒),产生自然的减速效果。
function spin() {
let c = 0;
const t = 12 + Math.floor(
Math.random() * 8
);
(function tk() {
const w = A[Math.floor(
Math.random() * A.length
)];
sW.textContent = w;
c++;
if (c < t) {
setTimeout(tk,
60 + (c/t) * 250
);
}
})();
}
从零开始计数。
随机选一个总"转数",在 12 到 19 之间。
定义一个内部函数,每次执行一"跳":
从 201 个词里随机选一个。
把它显示在转圈框里。
计数器加 1。
如果还没到目标次数...
安排下一跳。延迟公式:开始时 60ms,逐渐增加到约 310ms。这就产生了减速效果。
魔法在这个表达式里:60 + (c/t) * 250
60 + (0/15) × 250 = 60ms — 飞快
60 + (7/15) × 250 = 177ms — 明显变慢了
60 + (14/15) × 250 = 293ms — 落定前的戏剧性停顿
公式 起始值 + (进度) * 范围 叫做"线性插值"(lerp)。它能在两个值之间平滑过渡。这个模式在动画、游戏和数据可视化领域无处不在。
在词快速闪过时,一个小图标在四个 Unicode 四分之一圆弧字符之间循环切换,制造旋转效果:
◐ ◓ ◑ ◒
一个 setInterval 每 150ms 切换一次。就是四个字符轮流登场——最简单的动画。
一个全屏浮层,支持中英文搜索 201 个词
在搜索能工作之前,代码会预先构建一个扁平的列表,包含每个词的全部元数据:
const SD = D.flatMap(c =>
c.w.map(w => ({
w,
zh: Z[w] || '',
em: E[w] || '',
cat: c.zh
}))
);
对 D 里的每一个类别...
...取出该类别里的每一个词...
...给它打包一个"小档案":
词本身、中文翻译、emoji、所属类别。
把所有档案拍平成一个大的可搜索列表。
flatMap 是二合一操作。map 把每个类别转成一堆小档案(产生"列表套列表")。flat 再把它们合并成一个扁平列表。当你需要把分组数据"展开"成一个统一集合时,这是最常用的模式。
过滤函数非常简洁——检查搜索词是否出现在英文名或中文翻译里:
const m = q
? SD.filter(d =>
d.w.toLowerCase().includes(q)
|| d.zh.includes(q)
)
: SD;
如果有搜索关键词...
只保留满足条件的词:
英文名包含关键词(不区分大小写)...
或者中文翻译包含关键词。
如果没有关键词,显示全部。
显示结果时,匹配的文字会用动态正则来高亮:
d.w.replace(
new RegExp(
'(' + escapedQuery + ')',
'gi'
),
'<mark>$1</mark>'
)
在词的文字中,找到所有与搜索词匹配的地方(不区分大小写,全局搜索)...
...把每个匹配包在 <mark> 标签里,浏览器会自动给它加上高亮背景色。
用 Set、进度条和里程碑弹窗把探索变成游戏
怎么知道用户已经看过哪些词了?你需要一种绝不会存重复的数据结构。请出 Set:
const seen = new Set();
function markSeen(w) {
if (seen.has(w)) return;
seen.add(w);
cNum.textContent = seen.size;
cFill.style.width =
(seen.size/TOTAL*100) + '%';
}
创建一个空的"来宾名单"(Set)。
当一个词被点击或转到时:
如果这个词已经在名单上了,啥也不做(不重复计数)。
否则,把它加到名单里。
更新屏幕上的计数数字。
拉伸进度条:(已看词数 / 总数)× 100%。
每 10 个词就会有一个小庆祝从屏幕顶部滑下来。里程碑被定义为一个简单的数组,包含阈值和消息:
const MS = [
[10, '🌱', '初来乍到'],
[50, '⭐', '半百达成'],
[100, '💯', '破百!'],
[201, '🏆', '全部集齐!'],
// ... 还有 10 个里程碑
];
const m = MS.find(
x => x[0] === n
);
if (m) showToast(m[1], m[2]);
定义里程碑检查点:10 个词显示 🌱,50 个显示 ⭐,100 个显示 💯,201 个显示 🏆。
每次计数变化时检查:当前数量有没有命中某个里程碑?
如果命中了,弹出一个带 emoji 和消息的通知。
弹窗一开始藏在屏幕上方(top: -60px)。触发时,一个 CSS 类让它滑入视线:
.toast {
position: fixed;
top: -60px;
transition: top .4s;
}
.toast.show {
top: 1rem;
}
// JavaScript 里:
toast.classList.add('show');
setTimeout(=>
toast.classList.remove('show')
, 2500);
弹窗住在屏幕上方,看不见的地方。
它的位置一旦变化就会用 0.4 秒平滑过渡。
加上"show"类时,从上方滑到距顶部 1rem 的位置。
在 JavaScript 里:加上这个类让它出现...
...2.5 秒后移除这个类,它就滑回上方消失了。
注意 JS 只负责加/移除一个类——它从不计算位置或帧。CSS 处理所有的动画数学。这种"切换类名,让 CSS 来做动画"的模式是构建 UI 动画最干净的方式。
你现在明白了 690 行 HTML、CSS 和 JavaScript 是如何创建一个有轮播、老虎机、双语搜索和游戏化的交互式词汇探索器的。
Data-driven UI、CSS transform、递归动画、Set 去重追踪、class toggle transitions、regex highlighting
打开在线探索器 看看实际效果
由 ππ 制作 — 课程由 Claude 生成