关于 fire-paper-loader
使用 Vanilla JavaScript、WebGL 和 Worker 构建的燃烧纸环加载动画技能,面向页面构建期间主线程可能阻塞的场景。它让火焰在 OffscreenCanvas 上持续绘制,并涵盖预热、进度衔接、淡出和无障碍状态。
主要功能
- 01Worker 与 OffscreenCanvas 绘制
- 02主线程阻塞时保持动画
- 03并行编译并预热着色器
- 04加载进度平滑衔接页面动画
- 05减少动态效果与进度状态支持
适用场景
- 01为加载时间较长的页面制作加载动画
- 02避免页面构建阻塞时动画停顿
- 03将加载进度衔接到页面开场动画
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill fire-paper-loader安装后在 Claude Code 中输入/fire-paper-loader确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
demo.htmlPROMPT.mdagents/openai.yaml详细介绍
火焰纸环加载器
页面加载时,黑色背景上燃起一圈纸火。其实现机制是:火焰由 Worker 在 OffscreenCanvas 上绘制,并从 <head> 启动,因此即使主线程阻塞数百毫秒,火焰也会持续燃烧。 火焰效果只是表象;无论外观如何,加载器停顿 450 ms 都会让人感觉页面卡住了。
最相关的技能:web-design-performance 涵盖通用页面速度优化;本技能专门讲解如何让加载器在主线程阻塞时继续运行。如果需要滚动过渡时的燃纸效果(不是加载器),其火焰 GLSL 也是 kbFlame,但由页面绘制。如果需要旋转指示器或 AI 状态光球,请使用 thinking-orbs。
根据源码验证的技术栈:原生 JavaScript、原始 WebGL 1(GLSL ES 1.00,加载器中不使用 Three.js)、Worker 加 OffscreenCanvas,以及用于标签和淡出的 CSS。
从 head 开始,而不是从 body 开始
症状:加载器晚 1–2 s 才出现;Worker 始终无法获取 GL 上下文;或者在 three.js 下载期间火焰静止不动。只有主线程空闲时,Worker 才能启动并创建 WebGL 上下文;此时机是在 <head> 中加载样式表期间,之后要等到页面解析完成才会再次出现。
- 在
<head>中的<script>里:创建 canvas,调用transferControlToOffscreen(),从一个小型引导脚本的 Blob URL 启动 Worker,然后发送{kbPre, canvas},并立即在 Worker 中调用getContext('webgl', {antialias:false, alpha:false, depth:false, stencil:false})。将 canvas 和 Worker 保存在window.__kbPre上。 - 引导脚本只是一个外壳。加载器自身的
<script>(位于 body 后部)会将其textContent通过{kbInit, src, canvas:null, t0, origin: performance.timeOrigin, reduced, view, target}发送给 Worker,由 Worker 对其执行eval。同一段源码在两个线程上运行:const IN_W = typeof document === 'undefined'。使用两份代码会导致它们逐渐不一致。 - 对每一步都做好保护:如果没有
Worker、OffscreenCanvas或transferControlToOffscreen,Worker 触发onerror,或者收到fail消息,就使用相同代码在主线程上绘制,并执行相同的预热流程。 - 转换时钟。Worker 的
performance.now()与页面不同:发送performance.timeOrigin,并在 Worker 中加上performance.timeOrigin - origin,这样页面读取的每个时间戳都属于页面自己的时钟。
并行编译、预热流水线,然后显示
症状:最初的可见帧是整个加载过程中最慢的(30–130 ms,在 Metal 上有一次达到 408 ms)。
- 创建两个程序(ring、cinders),附加着色器,为两个程序都调用
linkProgram,然后再查询状态。使用KHR_parallel_shader_compile时,驱动会在自己的线程中编译;逐个着色器读取COMPILE_STATUS会抵消这一优势。存在该扩展时,跳过逐个着色器检查。 warmUp():在 1x1 视口中运行一帧真实绘制,同时绘制两个程序,然后等待readPixels读取 1 个像素。首次绘制会构建 GPU 流水线;要在任何内容显示之前完成这一步。- 然后才启动
requestAnimationFrame。页面的LOAD.ready()会在 Worker 完成绘制后解析(通常需要 0.15–0.3 s,超时时间为 1.2 s);构建流程会等待它,这样主线程就不会被占用一秒,火焰也不会静止不动。 - 只显示一个 canvas,不要显示两个。在首批可见帧中让两个线程都绘制火焰,最初会耗费编译时间。
进度环的节奏,应按视觉效果来规划
症状:煤块的大小停滞一秒,然后猛增 2.5 倍;或者,一个停滞的环被更快的环追上,看起来像两团火。
- 在
S = gain + ln(radius / first radius)中规划增长,而不是直接按原始进度u规划。S 中的等幅步进在任何尺寸下看起来都像是等幅增长。淡入在 S 中占 0.3(当进度为 1 时,尺寸被压住了 0.9 秒)。 - 用临界阻尼弹簧让
S跟随目标值(角频率 5/s,速度限制为 1.6/s,以 1/120 s 为子步长积分)。目标值是真实进度,但绝不会落后于缓慢增长值:creep = 0.38*(1 - exp(-t/800 ms));等待期间,目标值不能超过WAIT0.290,并以 2.5 s 的时间常数平滑接近CEIL0.335。硬性停顿和成块到来的进度会让圆环看起来像是先停住,再突然被推了一把。 - 圆环绝不静止:最小速度
VMIN为 0.016/s(在 8 fps 下约为每帧 5 px),最大速度VMAX为 0.05/s。视觉半径:进度为 0 时是 4 px 的小光点,到 0.15 时增至 5.8 px,之后使用火焰自身的半径。 - 页面尚未显示任何内容前,进度会按自己的时钟缓慢增长,因此即使 three.js 仍在加载,圆环也会持续运转。
无停顿地交接
症状:圆环猛地跳入页面的开场动画;或者加载器消失时,页面卡顿 55-240 ms。
done()会在 S 空间中运行一段 Hermite 曲线,将速度从缓慢增长值平滑过渡到开场动画的速度V_HAND0.24(u/s),持续ds = clamp(2.8*du/vH, 0.30, 1.2)秒,并在圆环边缘到达交接进度HAND0.36 时发出通知。页面会从该确切进度和时钟继续自己的开场动画。如果 rAF 停滞,安全计时器(doneDur + 400 ms,页面端为 2.4 s)仍会解除等待。- 标签会到达 100,并在合成器上淡出(
transition: opacity .40s linear .12s):繁忙的首帧不会让它过早消失。 close():设置alive = false,并通知 worker 停止绘制。淡出期间,两个上下文同时绘制会争用 GPU,使淡出动画的每帧耗时达到 35-67 ms。最后一帧会继续显示,并通过 CSSopacity淡出(常规情况下为 120 ms,减少动态效果时为 250 ms);淡出过程使用 Web Animations 关键帧,沿着开场动画自身的曲线缩放,让圆环在页面上的边缘位置保持不变。- 页面仍在屏幕上时,不要调用
worker.terminate()。 它会在任意时刻阻塞主线程 55-240 ms。发送free(在 worker 线程中释放 GL 上下文),并且只在visibilitychange变为 hidden 或pagehide时终止 worker。也不要在主线程调用loseContext():它会等待所有排队的 GPU 工作完成,包括开场动画的首帧(100 ms)。 - 在各构建阶段之间主动让出执行权:
await LOAD.step(p)会设置进度,并且最多每隔 45 ms(worker 绘制时为 90 ms,因为它只需要更新计数)等待一次requestAnimationFrame,再等待setTimeout 0。在隐藏标签页中不会等待,因为 rAF 不会运行。
燃烧效果:页脚效果是参照
所有者按顺序提出的症状:“太静态了”“黑色部分应该使用 Alpha 遮罩”,然后在尝试了四级海报化渐变后又说:“太不真实了,还是之前的效果更好,页脚的燃烧效果更好”。
- 发光渐变,而非海报化阶梯。 余烬区域从黄色平滑过渡到橙色,再到深红色;高温区将黄色向后延伸,最后以清晰、撕裂状的边缘收尾。生硬的阶梯看起来像海报。
- 前沿有一条明亮的珠状细线,宽 0.7-5 px,沿线粗细收缩变化(均匀的线条看起来像霓虹灯),并带有紧凑的 2-5 px 加法辉光。不能更宽:过大的辉光会让边缘变成霓虹光环。
- 它必须动起来。 热脉冲沿边缘双向传播(
run),局部区域以 7 Hz 闪烁(flk),珠状光点漂移,翻卷的热浪向后掠过区域。静帧看起来没问题;加载器的效果要看动态表现。 - 金色,而非奶油色。 最亮核心颜色为
(1.85, 1.02, 0.30),余烬颜色最高为(1.10, 0.64, 0.17),且只出现在高温区域。奶油色和卡其色是因为亮部变白或通过调暗整体来降低亮度;较暗的区域应沿渐变过渡至深红色。 - 多孔炭化层,不要描边。 余烬后方的炭化带接近黑色
(0.0085, 0.0062, 0.0048),带有纤维纹理;孔洞会随时间形成,并逐渐露出下方的画面。绝不要用发光边缘勾勒孔洞:它们会连成沿边缘延伸的一整条轮廓。 - 圆环绝不能从圆盘或字母“o”开始。 它从一段撕裂状的余烬弧开始,厚 3.5-7 px,略多于半圆,两端沿圆周爬行并在 6.5 到 12 px 之间相接。圆环宽度绝不能低于 3.5-7 px,否则看起来像描出的轮廓。
- 余烬飞屑:四组实例(1800、720、360、160 个四边形,使用种子为
0x2f6b9d1的 mulberry32),仅从上方弧段(20-160 度)飞出,存续时间短,颜色从炽热橙色到深红色,绝不变白。先使用 ACES 色调映射,再在着色器中转换为 sRGB(aces然后srgb),因此将gl_FragColor的 Alpha 设为 1,并将画布设为alpha:false。
实测开销
- 在主线程上绘制时,如果构建过程会一次阻塞主线程 250-450 ms,火焰帧之间最长间隔为 450 ms。在同一页面的 Worker 中则为 9 ms。
- 渲染 DPR 最高为 1.5(此处源码的上限;Web 通用上限为 2)。圆环由一个全屏三角形和 3040 个余烬四边形组成;开销来自片元着色器,而非余烬飞屑。
- 时间步进:Worker 按自身帧绘制;显示时钟属于页面,因此动画的 dt 会按设计进行限制(
dtp最大为 1.5 s,弹簧子步进为 1/120 s)。
生命周期与无障碍
- 标签上使用
role="progressbar",并保持aria-valuenow同步;画布设为aria-hidden。计数以文本形式呈现,因此即使没有火焰效果也能读取。 prefers-reduced-motion: reduce:不推进时间(tSec = 0),不显示余烬飞屑,不进行合成器缩放;圆环仍会随进度展开,呈现静止且带纹理的边缘,淡出时长为 250 ms。画面按静态效果设计,而不是隐藏起来。- 标签页隐藏时:两个线程中的 rAF 都会暂停;
step()停止等待;done()会立即完成,且只在此时终止 Worker。 - 进度低于 40% 且持续 12 s 后,标签会显示“仍在引燃”:应告知用户连接较慢,而不是让加载过程悄无声息地无限持续。
- 调整尺寸:向 Worker 发送新的
view(w、h、DPR);画布会在下一帧调整尺寸。
此技能的适用范围
保留机制:提前启动 Worker、同一份源码运行于两个线程、并行编译并等待 1 px 预热、S 空间节奏控制、Hermite 交接、先停止再用 CSS 淡出、不提前终止。可自由替换舞台呈现:火焰的调色板和形状、标签文案,以及它交接到的页面。演示将页面的开场动画作为替代:done() 之后,加载器的圆环会沿开场曲线继续运行 1.8 s,直到离开画面;此时真实页面会根据交接进度绘制自己的边缘。
来源:摘自 KIBORI 木彫 的 KINDLING 加载器。该页面使用滚动联动的 Three.js 木工展示,1.1 MB 的构建产物会让主线程最多阻塞 1.2 s。