ChunLong Blog

ChunLong Blog

LOADING

实验室扩容记:从一片星海到六个玩具

2026-09-15969 字 · 约 3 分钟0 次阅读

实验室(/lab)最初的样子:一片 three.js 星海,八颗行星挂着站点的入口,小行星带里漂着访客留下的星。后来加了第一个实验台玩具——烟花。然后就停在这里很久。

直到最近一次性扩到六个玩具:孔明灯、粒子文字、流体模拟、代码雨、白噪音机。这篇文章记的是扩容的过程,以及比玩具本身更重要的东西——让"加一个玩具"变成十分钟的事。

星海时代的设计遗产

星海上线时埋了一个好架构:3D 场景归 3D 场景,实验台玩具走一套独立的注册表。注册表分两层:

  • 数据层:一个纯数据的数组,每条记录 slug、emoji、四语言的名称和简介。页面的 metadata、sitemap、实验台卡片、成就判定全部从这份表读;
  • 组件层:slug 到组件的映射,用 next/dynamic({ ssr: false }) 懒加载——demo 的代码不会进 /lab 主页的包。

于是"新增一个实验"被压缩成了公式:写组件 + 注册表加一行 + 加一条元信息。路由 /lab/[slug]、进页埋点、demo_all 成就的判定阈值(LAB_DEMOS.length 动态推导)全自动跟随。

TIP

判定阈值从数据源动态推导而不是写死数字,是这次最划算的决定——成就门槛从 1 变 6 的过程中,相关代码一行没改。

五个玩具的取舍

每个玩具进入实验台前都要过一遍同一份工程约束清单,这是烟花时代立下的规矩:

  • 纯 Canvas 2D(流体用 WebGL),不引 3D 库;
  • requestAnimationFrame 空闲即停——没有东西可画就不烧电;
  • DPR 钳制 ≤2;粒子上限封顶;
  • prefers-reduced-motion 降级;
  • 画布 touch-action: none,触屏玩不触发滚动。

在这份约束下,五个玩具各有各的故事:

玩具关键实现取舍
🏮 孔明灯5 档色相 sprite 预渲染 + 正弦横漂愿望纯本地不上传——和留声星划清边界
✨ 粒子文字离屏 fillText + getImageData 采样目标点点数钳制 1600,超了自动加大采样步长
🌊 流体Jos Stam 稳定流体的 WebGL 精简版砍掉 bloom/光照后处理,模拟 128 染料 256 够看
🌐 代码雨列式字符雨 + 半透明黑拖尾几乎零成本,纯氛围
🎧 白噪音机合成 + 采样双引擎详见另一篇素材课

流体是工作量最大的一个。参考了 MIT 开源的 WebGL-Fluid-Simulation 思路自写精简版:半拉格朗日平流 → 散度 → Jacobi 迭代解压力(20 次)→ 梯度减除。WebGL2 优先、WebGL1 half-float 回退、全都不支持就显示降级提示——每个玩具都得有退路,这是我给实验台立的另一条规矩。

注册表之外的两件小事

主题色联动。所有 demo 的配色都从一个探针函数取站点主题色——往页面里插一个带 text-accent 类的隐藏元素,读 getComputedStyle 的计算色,失败回落默认暖金。于是换主题色不只是换按钮颜色,烟花、代码雨、流体的起点色相都会跟着变。

成就的暗坑。扩容时发现访客"玩过的实验"列表只保留最近 10 条——demo 数量逼近 10 时,demo_all 成就会静默失效。顺手把上限放宽到 20 并留了注释。这种"配置数字和另一处数据规模暗暗耦合"的坑,值得每个做功能开关表的人警惕。

下一个玩具

注册表还空着很多位置:排序可视化、生命游戏、万花筒、重力沙……候选名单在 ROADMAP 里排着队。体系搭好之后,加玩具的快乐变得很纯粹——只需要想"做什么好玩",不用再想"怎么接进来"。

这大概就是架构的意义:把重复的部分沉淀掉,让剩下的部分只关于创意。

评论