Skip to content

给首页加粒子连线背景动效(Constellation)

很多人觉得“那种线段不断连接”的动效很酷,但不确定静态站能不能做。能——它是纯前端 Canvas + JS,不需要任何后端。这篇记录我给 OpenGood 首页加的这个粒子连线背景。

经典「粒子网络」:一堆小节点缓慢漂移,相邻节点之间连线,距离越近线越亮。

选型上没上 tsparticles(功能多但 ~100KB+,重),而是手写 ~70 行原生 Canvas:更轻、完全可控、无依赖。

新建 src/components/Constellation.astro,本质上就一个 <script>:

const canvas = document.createElement('canvas');
canvas.style.cssText =
'position:fixed;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none;opacity:.55';
document.body.prepend(canvas);
// …节点数组 + requestAnimationFrame 循环:移动、边缘反弹、相邻连线、画节点

首页 index.mdx 引入:

import Constellation from '../../components/Constellation.astro';
<Constellation />

关键点一:为什么 prepend 到 <body> + z-index:-1

Section titled “关键点一:为什么 prepend 到 <body> + z-index:-1”

这是能不能“看见”的关键。目标是画布在底色之上、正文之下。

  • 背景传播:Starlight 把页面底色设在 body 上(reset.css 里 body { background-color: var(--sl-color-bg) })。按 CSS 规则,<html> 无背景时,<body> 的背景会传播到视口(canvas)——等于底色画在最底层,body 本身变透明。
  • 所以把画布作为 body 的直接子节点、z-index:-1,它就稳定画在「视口底色」之上、「正文流」之下,看得见又不挡字。
  • 如果把画布放进某个中间容器,而那容器有不透明背景,就会把画布盖住——这就是为什么用 document.body.prepend() 直接挂到 body。

关键点二:颜色读 CSS 变量,暗/亮自适应

Section titled “关键点二:颜色读 CSS 变量,暗/亮自适应”
const accent = () =>
getComputedStyle(document.documentElement)
.getPropertyValue('--sl-color-accent')
.trim() || '#1ae686';

每帧读 --sl-color-accent(OpenGood 翠青绿),主题切换时颜色自动跟着变。

  • prefers-reduced-motion:系统开了“减少动效”时降速 + 降亮(速度 ×0.35、透明度 ×0.6),保留轻微运动而非冻成静态——既不晃眼,又不至于让画面僵死。
  • requestAnimationFrame + 切后台 visibilitychange 暂停,不空耗。
  • devicePixelRatio 缩放,高清屏不糊;节点数按视口面积算、封顶 90,O(n²) 连线检查也就几千次/帧,毫无压力。
  • pointer-events:none + aria-hidden:纯装饰,不挡点击、屏幕阅读器忽略。

Astro 会把组件里的 <script> 只打进首页的产物——小脚本默认被内联进首页 HTML,只有渲染了 <Constellation /> 的页面才带这段脚本,内容页零开销。

关键点五:ClientRouter 下要幂等(导航重跑)

Section titled “关键点五:ClientRouter 下要幂等(导航重跑)”

上一节说脚本被「内联进首页 HTML」,而 Starlight 默认开启 ClientRouter(View Transitions)做无刷新导航——内联 <script> 在每次导航都会重新执行(外链 module 才只跑一次)。

后果:每次切回首页都会再 prepend 一个 canvas、再绑一组 resize / visibilitychange 监听、再起一个 rAF 循环,旧的却不清——越积越亮、CPU 空耗。

解法是把特效做成幂等实例:每次进来先 destroy() 上一份(取消 rAF、解绑监听、移除 canvas),再建新的:

if (document.body.__ogConstellation) document.body.__ogConstellation.destroy();
// …建 canvas、起循环…
document.body.__ogConstellation = {
destroy() {
cancelAnimationFrame(raf);
window.removeEventListener('resize', resize);
document.removeEventListener('visibilitychange', onVisibility);
canvas.remove();
delete document.body.__ogConstellation;
},
};
  • 静态站完全能做动态视觉效果,纯前端 Canvas 即可,无需后端。
  • 画布 prepend 到 body + z-index:-1,靠 CSS 背景传播规则稳稳落在底色与正文之间。
  • 无障碍(reduced-motion 改为降速降亮而非静态)+ 性能(rAF / DPR / 暂停 / 封顶)+ 按页加载,都顾上了。
  • ClientRouter 下用幂等实例防导航重跑导致的 canvas / 监听 / rAF 堆积。
  • 2026-08-07:prefers-reduced-motion 由「只画一帧静态」改为「降速降亮仍运动」(修复首页在系统开启「减少动效」时画面僵死);并补上幂等 destroy(),解决 ClientRouter 无刷新导航重复执行造成的 canvas 与监听器堆积。