其他
在网页中实现粒子、彩带、雪、烟、火花、流场或星座连接背景时…
适用于需要浏览器自包含交付的粒子动画、发射器、流场/curl noise、连接点网络或 Three.js GPU 千级粒子效果,并要求可复现截图验证的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 48/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 57/100 ×0.15
适用场景
在网页中实现粒子、彩带、雪、烟、火花、流通用
解决思路
- 选型:数百个粒子用 2D canvas,数千个以上用 GPU Points。
- 核心循环:每个粒子保存状态,每帧推进。
- 每帧把所有 force 函数返回的 [fx,fy] 累加成 ax、ay。
- 积分粒子:vx += ax*dt; vy += ay*dt; vx *= 0.99; vy *= 0.99; x += vx*dt; y += vy*dt; life -= dt*0.2; life<=0 时 reset 或移除。
- 使用半隐式欧拉:先更新速度,再用新速度更新位置,对弹簧/阻力稳定。
- 使用固定或 clamp 的 dt:Math.min(dt, 1/30),避免切换标签后模拟爆炸。
- 力定义为返回加速度 [fx,fy] 的函数,并组合成列表。
- gravity:常量向下,() => [0, 400]。
- drag:比例阻力,(p) => [-p.vx*0.5, -p.vy*0.5]。
- attract(tx, ty, strength):dx=tx-p.x; dy=ty-p.y; d2=dx*dx+dy*dy+100; f=strength/d2; 返回 [dx*f, dy*f]; +100 用来软化奇点。
- repulsion 用负 strength;朝 home 位置的 spring 给 settle back 效果。
- flow field:noise2D 返回 -1..1;angle=noise2D(p.x*scale, p.y*scale)*Math.PI*2;返回 [Math.cos(angle)*speed - p.vx, Math.sin(angle)*speed - p.vy]。
- 真 curl noise:有限差分,n1=noise2D(x, y+eps), n2=noise2D(x, y-eps), n3=noise2D(x+eps, y), n4=noise2D(x-eps, y); 返回 [(n1-n2)/(2*eps), -(n3-n4)/(2*eps)],得到无散度、流体感场。
- 让场随时间演化:把时间加到 noise 输入,noise2D(x*scale, y*scale + t)。
- Burst 发射:一次在一点生成 N 个粒子,角度/速度在锥内随机;之后交给 gravity+drag;无 respawn,死亡后移除。
- Continuous 发射:稳定速率生成粒子;死亡粒子在顶部或源处重生。
- 随机范围:angle=base+(Math.random()-0.5)*spread; speed=min+Math.random()*(max-min)。
- Burst 示例:n=120; a=Math.random()*Math.PI*2; s=200+Math.random()*400; vx=Math.cos(a)*s; vy=Math.sin(a)*s-200; life=1; size=4+Math.random()*4; color=`hsl(${Math.random()*360},90%,60%)`; rot=Math.random()*6.28; vr=(Math.random()-0.5)*10。
- Confetti 观感:旋转 + 扁平矩形 + gravity + air drag,而不是圆点。
- Snow 观感:缓慢下落 + 水平正弦摆动 + 大小变化制造景深。
- 连接点网络避免 O(n^2):使用 uniform spatial grid,cell=radius,cols=Math.ceil(W/cell),Map,key(cx,cy)=cx+cy*cols。
- 按 cell 分桶:cx=(p.x/cell)|0; cy=(p.y/cell)|0; 把粒子推入 grid。
- 对每个粒子只检查 8 个邻域(oy=-1..1, ox=-1..1)。
- 对 bucket 中的 q 排除自身,dx=p.x-q.x; dy=p.y-q.y; d=Math.hypot(dx,dy); 若 d<radius,设置 ctx.globalAlpha=1-d/radius,画 p 到 q 的线。
- 画完后恢复 ctx.globalAlpha=1;均匀分布时为 O(n);每对会找到两次,可只检查前向邻居来减半。
- GPU 规模:千级以上用 Three.js Points + BufferGeometry + ShaderMaterial。
- 创建 N=50000 的 pos Float32Array(N*3),随机 (Math.random()-0.5)*20;geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));。
- ShaderMaterial uniforms: u_time {value:0}, u_size {value:6}; transparent:true, depthWrite:false, blending:THREE.AdditiveBlending。
- vertexShader:p=position; p.y += sin(u_time+position.x)*0.5; mv=modelViewMatrix*vec4(p,1.0); gl_PointSize=u_size*(10.0/-mv.z); gl_Position=projectionMatrix*mv。
- fragmentShader:d=length(gl_PointCoord-0.5); if(d>0.5) discard; gl_FragColor=vec4(1.0,0.8,0.4,smoothstep(0.5,0.0,d))。
- 添加到场景:scene.add(new THREE.Points(geo, mat));循环中 mat.uniforms.u_time.value=clock.getElapsedTime()。
- AdditiveBlending + depthWrite:false 给发光粒子;discard 让方形点变圆;每粒子数据添加 custom attributes,在 shader 中读取。
- 交付为 standalone HTML:一个文件直接在浏览器打开;canvas 2D 内联,或 Three.js 从 CDN 通过 importmap 加载 GPU Points;一个 render loop;无 build step。
- Output contract:一个 .html;canvas 用内联 <script> 做模拟 + 2D 绘制循环;GPU points 用 importmap pin three 到 CDN,并把 Points setup 内联。
- 用一个累计 time 驱动模拟:累加 clamped dt,或 GPU 用 clock.getElapsedTime()/u_time;不要每个粒子散用 Date.now()。
- Seed RNG:替换裸 Math.random() 为 seeded PRNG(如 mulberry32),让 spawn positions、angles、bursts 可逐帧复现。
- Seek/freeze harness:?t=N 重新播种,用固定时间步确定性推进到 N 秒,渲染一次并停止 loop。
- Harness 实现:let rng=mulberry32(1234); const particles=spawn(()=>rng()); function render(){...}; 读取 t=URLSearchParams(location.search).get('t');若 t!==null:step=1/60, end=parseFloat(t); for(s=0;s<end;s+=step) update(step); render(); window.__ready=true; 否则 rAF loop update(Math.min((now-prev)/1000, 1/30)); render();。
- 验证循环:在 start、mid、settle 打开 ?t=0、?t=<mid>、?t=<end>;burst 用 t≈0 spawn、t≈0.5 spread、t≈1.5 settle;各截图。
- 检查 fidelity(符合 brief)和 artifacts:blank canvas = parse/init error(查 console);粒子逃出帧 = 缺 clamp/wrap;NaN positions = 全部消失;所有粒子堆在原点 = RNG 未接好。
- GPU points 的 WebGL 需要 GPU context;Playwright/Chromium headless 提供 swiftshader。
- 截图命令:npx playwright screenshot --wait-for-timeout=600 "file://$PWD/particles.html?t=1.0" frame-mid.png。
- 打包 helper:scripts/seek-shot.sh anim.html 0 1.5 3 冻结 ?t=N harness 并截图每个时刻;scripts/contact-sheet.sh sheet.png frame-*.png 平铺供一眼审查;见 scripts/README.md。
- 完成前检查 1:canvas 渲染粒子,不空白,无 console/WebGL 错误,无 CDN 404。
- 完成前检查 2:?t=N 冻结可复现帧(seeded RNG + fixed timestep → 相同 N → 相同像素)。
- 完成前检查 3:start/mid/settle 截图符合 brief,无 escaped/NaN/origin-bunched 粒子。
- 完成前检查 4:若嵌入 SPA,必须 disposed/leak-free:cancel rAF loop;GPU 释放 geometry/material/renderer。
- 完成前检查 5:尊重 prefers-reduced-motion,减少粒子或改成静态场。
- Quick reference 配方:Confetti=burst+gravity+drag+rotating rects;Snow=continuous top spawn+slow fall+sine sway;Smoke=continuous+upward+grow size+fade alpha;Sparks=short-life burst+additive+fast fade;Flow field=noise angle→velocity,随时间演化;Curl noise=curl of noise potential(divergence-free);Constellation=spatial grid,link within radius,fade by distance;1000s+=Three.js Points + ShaderMaterial,在 vertex shader 动画。
- 参考文件:references/particle-recipes.md 有完整 canvas confetti、snow、smoke 系统;mouse attraction/repulsion;完整 simplex flow-field 和 curl-noise field;spatial-grid connected-dot background;GPU Points 系统含 per-particle life/seed attributes、additive glow、shader respawn。
适用边界 · 注意事项
- 粒子数达到数千/数万时,不要继续用 2D canvas 逐粒子绘制;改用 Three.js Points + ShaderMaterial 在 GPU 上动画。
- 连接点网络中粒子超过约 300 个时,不要使用全量 O(n^2) 两两比较;改用 uniform spatial grid,只比较相邻 cell。
- 单个自包含粒子效果不要引入 bundler/build step;一个可直接打开的 .html 就够。