关于 falling-leaves
用于在 2D Canvas 或 3D 场景中制作可辨识落叶效果的设计技能,涵盖翻滚、空气滑移、分层景深、生命周期管理和减少动态效果支持。
主要功能
- 01叶片侧转至边缘的翻滚
- 02滑移与翻滚角度联动
- 03区分正反面的精灵图
- 04为每片叶子设置不同运动参数
- 05按视口调整回收与密度
- 06生命周期与减少动态效果处理
适用场景
- 01在 2D Canvas 中制作可辨识的落叶
- 02在 3D 场景中构建分层落叶效果
- 03按屏幕尺寸调整叶片密度
- 04为减少动态效果设计静态画面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill falling-leaves安装后在 Claude Code 中输入/falling-leaves确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamldemo/index.htmlPROMPT.md详细介绍
飘落的叶子
让飘落的物体看起来像叶子。想要营造范围有限、由通用微粒构成的氛围时,请使用 ambient-section-particles。如果形状必须清晰可辨,就使用这个。
先做好翻转效果
让每片叶子绕自身长轴旋转,正面朝向观察者,侧边朝向观察者时薄至消失,然后在另一侧重新展开。那一瞬间近乎消失的效果,正是眼睛辨认出“叶子”的关键。无论美术效果多好,只在画面平面内旋转的精灵看起来都像五彩纸屑、硬币或纸片。
在二维画布上,翻转表现为水平缩放穿过零:
ctx.save();
ctx.translate(l.x, l.y);
ctx.rotate(l.roll); // long axis drifting in-plane
ctx.scale(Math.cos(l.spin), 1); // the tumble: cos crosses 0, edge-on
ctx.globalAlpha = l.alpha;
ctx.drawImage(sprite, -w / 2, -h / 2, w, h);
ctx.restore();
驱动两个轴,而不是一个。roll 让叶子在画面平面内旋转;spin 让叶子穿过画面平面旋转。为每片叶子的两个轴分别设置速率,否则无论缓动效果如何,运动都会显得机械。
在三维中,使用实例化的四边形,绝不要使用点精灵。点精灵始终面向摄像机,永远无法转到侧边朝向摄像机,因此也就永远无法呈现侧面。仅此一项限制就决定了整个实现方式。
让侧滑与翻转联动
根据翻转角度驱动横向运动,并让二者相位相差 90 度。叶子侧边朝向观察者、切入气流时会横向滑动,正面平展时则会停滞:
l.x += Math.sin(l.spin) * l.slip * dt; // fastest when cos(spin) ≈ 0
l.y += l.fall * dt;
不要在 x 上叠加独立的正弦波。那会让效果看起来像风,或是缓动出错。这种联动只需增加一项,就能让轨迹显得符合空气动力学。
烘焙正反两面
为每种颜色烘焙两张精灵图,并根据翻转方向选择:
const img = Math.cos(l.spin) < 0 ? sprite.back : sprite.face;
让背面比正面更暗、更淡。否则叶子看起来就像旋转的平面剪纸;有了明暗差异,它才会像有正反面的实体。这是系统中成本最低的真实感提升方式。
为每片叶子设置不同参数
在生成时随机设置下落速度、翻转速率、滚转速率、侧滑幅度、相位、缩放和不透明度。任何一个参数都不要共用,否则叶片效果就会退化成沿屏幕滚动的纹理。眼睛大约两秒就能发现共同的节奏。
选择叶子的重新出现位置
这决定了屏幕上实际有多少片叶子。
在二维中,让叶子在视口范围内循环。当叶子经过底部时,在顶部上方以新的随机 x 坐标重新生成。也要让 x 坐标循环,这样风就不会让一侧空空如也。
在三维中,让叶子在摄像机前方循环,而不是围着摄像机循环。以摄像机为中心的带状区域,大部分体积都在视锥体后方和侧面;对于 36° 的摄像机,几百片叶子中只有十来片能进入画面。改为将它们放入沿摄像机视线向下延伸的圆盘中,同样数量的叶子在画面中就会多出好几倍:
camera.getWorldDirection(fwd); fwd.y = 0; fwd.normalize();
const cx = camera.position.x + fwd.x * AHEAD;
const cz = camera.position.z + fwd.z * AHEAD;
const a = Math.random() * TAU, r = Math.sqrt(Math.random()) * SPREAD;
l.x = cx + Math.cos(a) * r; l.z = cz + Math.sin(a) * r; l.y = camera.position.y + 16;
保留远处循环机制作为后备,以应对摄像机走出自身天气范围的情况;并将其放在雾效范围之外,以免看到叶子突然跳转。
根据带状区域面积而非数量设置密度
屏幕上的密度与数量 ÷ 带状区域面积成正比。增加数量之前,先缩小带状区域:
- 循环半径减半,在数量不变时,屏幕上的密度会变为四倍。
- 数量翻倍,在屏幕上的增益相同的情况下,绘制成本也会翻倍。
按视口面积缩放设定的数量,而不要照搬原始数值;否则,桌面端看起来舒缓飘落的效果,在手机上就会变成暴风雪:
const k = clamp(Math.sqrt((W * H) / (1440 * 900)), .5, 1.3);
const n = Math.round(authored * layerShare * k);
分层营造景深
使用两到三层,每层分别设置缩放、速度、不透明度和模糊:
| 层 | 缩放 | 下落速度 | 不透明度 | 备注 |
|---|---|---|---|---|
| 远景 | 0.3–0.5 | 慢 | 0.22–0.40 | 最先绘制,可置于内容后方 |
| 中景 | 0.5–0.85 | 中等 | 0.46–0.78 | 效果的主体 |
| 近景 | 1.05–1.9 | 快 | 0.50–0.82 | 数量少,可选择模糊,绘制在内容上方 |
让近景叶片从文字前方掠过。这种交错效果就是景深线索。这里放两三片叶子就够了,不要做成一道帘幕。
处理颜色和光照
- 从一个小范围的色阶中为每片叶子取样——从深牛血红到朱红,再到干琥珀色,并让每片叶子的饱和度有所不同。每片叶子都用同一种红色,一眼就能看出来。
- 3-D 叶片使用 Alpha 测试,而不是 Alpha 混合,这样它们在任何角度下都能正确排序,无需逐帧进行深度排序。
- 经过色调映射合成后,发光的红色会变成粉色。 如果叶片在暗场景中自发光,请将绿色和蓝色设为零(
0x780200,而不是0x8c1410),否则整个秋景都会变成糖果色。
控制开销
逐片更新的开销可以忽略不计;几百片叶子的三角函数运算,连 CPU 分析器的采样阈值都达不到。开销主要来自像素填充和绘制:
- 2-D canvas:每片叶子调用一次
drawImage。将精灵预渲染一次,尺寸取你会绘制的最大值。切勿逐帧重新绘制叶片路径。 - 3-D:使用一个
InstancedMesh,将矩阵合成为共享的Matrix4。将临时用的Matrix4/Quaternion/Euler/Vector3提到模块作用域。 - Alpha 测试会使叶片无法利用早期 Z 测试,因此其每像素开销会高于三角形数量所暗示的水平。宁可使用更多、更小的叶片,也不要使用更少、更大的叶片。
停止不可见时的工作
- 在
document.hidden或所在区段离开视口时(IntersectionObserver)暂停。恢复时重置lastTime,避免恢复后的第一帧累积整个暂停期间的时间。 - 将
dt限制在约 1/30 秒,避免卡顿导致场景瞬间跳跃。 - 将设备像素比上限设为 2。
- 使用根元素上的
ResizeObserver来设置尺寸,而不要只在脚本运行时测量一次。若页面稍后才完成布局,canvas 就会永远保持 0×0,因为它等待的 resize 事件已经触发过了。 - 构建时要防止视口尺寸为零。若控件通过一次性运行来接入,就可能在首次布局前调用构建器;若不做保护,整个场景都会堆在原点。
尊重读者
当 prefers-reduced-motion: reduce 生效时,渲染一帧构图良好的静态画面,不要播放动画。不要直接隐藏叶片;构图本来就包含它们。控件变化时重新绘制这帧静态画面,让控件仍然能发挥作用。
验证
- 叶片翻转时会经过侧对镜头的状态;叶片会明显变薄,并在每次翻转时消失一次
- 滑移由翻转角度驱动,而不是由独立的正弦函数驱动
- 正面和背面有所不同
- 每个参数都因叶片而异
- 在增加数量之前,尽可能缩紧回收带
- 3-D:在镜头前方回收,而不是绕着镜头回收
- 3-D:发光红色的绿色和蓝色设为零
- 数量随视口面积变化;读数显示实际构建的数量
- 隐藏或离开屏幕时暂停;
dt已限制;DPR 上限为 2 - 减少动态效果时显示经过设计的静态画面
- 在 390px 和 1440px 下控制台无错误