返回首页

用 Canvas 手写一个会旋转的银河粒子背景

你现在看到的背景就是它——一个约 200 行的 Canvas 2D 程序:对数螺旋生成旋臂、 倾斜投影制造立体感、加色混合替代后期处理,全程不依赖任何三方库。

先拆解:一个银河由什么组成

真实的旋涡星系有三个可见结构:中心的核球(暖色、厚重)、 盘面上的旋臂(年轻蓝星密集,夹杂电离氢区的红色)、 以及最外层的弥漫晕(稀疏、暗淡)。把这三种分布各写一个采样函数, 银河的骨架就出来了。

  • 核球:r = random()^1.9 × R,角向均匀,y 轴厚度较大,颜色偏暖黄。
  • 旋臂:半径服从指数盘 r = -ln(1-u) × h,角度沿对数螺旋排布。
  • 晕族:半径平方根分布 + 随机角度,亮度压到最低,负责“垫底”。

旋臂的形状:对数螺旋

对数螺旋的公式是 θ = arm + k · ln(r)。它的妙处在于,无论放大多少倍, 形状都不变——所以从中心到边缘的弯曲程度是自然连续的,不会出现直胳膊。

const arm = i % ARMS;
const r   = -Math.log(1 - Math.random() * 0.968) * 0.245;

/* 旋臂不是一条线:越往外越宽,用正态分布做抖动 */
const off = gauss() * (0.26 + 0.62 * r);
const th  = arm * (TAU / ARMS) + 2.35 * Math.log(1 + r * 5.5) + off;

注意 Math.log(1 + r × 5.5) 里的“1 +”是必须的: 它让半径趋近 0 时角度也趋近 0,四条旋臂才会干净地汇聚到核心,而不是在小半径处炸开。

关键一步:把盘面「立起来」

星系的坐标都在盘面 (x, z) 上,要看起来像被斜着观察,需要绕屏幕水平轴做一次旋转。 设观察仰角为 V,投影公式只有两行:

// sy:屏幕纵向位置;dz:朝向观众的深度(用于近大远小)
const sy = y * Math.cos(V) + z * Math.sin(V);
const dz = z * Math.cos(V) - y * Math.sin(V);

// 深度直接喂给尺寸和透明度,立体感就出来了
const size  = base * (1 + dz * 0.42);
const alpha = baseA * clamp(1 + dz * 0.42, 0.4, 1.4);

验证一下:V = 90°sy = z,盘面正对观众(正视); V = 0°sy = y,只剩厚度可见(侧视成一条线)。 本站取 V ≈ 58°,是介于两者之间最有立体感的角度,再叠加鼠标纵向位移做微调。

为什么不用 WebGL,加色混合就够了

粒子效果想要“发光”,最省事的做法是开启加色混合,让重叠的粒子亮度自然累加:

ctx.globalCompositeOperation = 'lighter';  // 等价于加法混合,天然 bloom
ctx.drawImage(sprite, px - s / 2, py - s / 2, s, s);

每个粒子都用一张预渲染的 64×64 径向渐变精灵,而不是 arc() 逐帧画圆。 这样既拿到了柔和光晕,又把每帧的路径构建成本降到了零——2600 个粒子在 60fps 下仍有余量。

drawImage 缩放小尺寸精灵时,浏览器会做双线性插值, 边缘反而比 arc() 的硬边更干净,这是意外收获。

旋转不缠绕:密度波近似

如果让每颗恒星按自己的角速度旋转(开普勒式),旋臂会在几十秒内被越搅越糊。 真实星系靠密度波维持形状,而工程上的简化做法是: 整盘做刚性旋转,只给核球单独提速。因为核球角度本来就是均匀分布, 提速不会被看出来,反而让核心区域显得更有生气。

// 每帧只对整盘算一次 cos / sin,然后所有恒星做线性变换
const cg = Math.cos(angle), sg = Math.sin(angle);
const x3 = bx * cg - bz * sg;
const z3 = bx * sg + bz * cg;

这样一帧里真正调用三角函数的次数是个位数,而不是 2600 次。 这也是整个方案能在低端手机上跑满帧率的核心原因。

性能与可访问性兜底

  1. 粒子总数按视口面积自适应:clamp(area / 620, 1100, 5200),移动端再乘 0.72。
  2. 像素比上限 2;连续掉帧时自动降到 0.75 倍分辨率并重建星群。
  3. 页面切到后台(visibilitychange)暂停动画,切回来再继续,避免白白耗电。
  4. 支持 prefers-reduced-motion:命中时只渲染一帧静态银河,不启动循环。

完整实现就在本站的 assets/js/galaxy.js 里,注释比代码还多一点, 可以直接下载过去改参数——试试把 CFG.arms 改成 2 或 6, 会得到完全不同气质的星系。

返回文章列表

最后更新于 2026-09-10 · 原创内容,转载请注明出处