中文 · English
一片会流动的星空:基于 three.js + postprocessing 的星星粒子系统。一座螺旋星系随滚动翻转、散成两侧星轨, 再聚成光标和结;同一批星星还能摆成任意文字和图标。粒子的分布、流动、辉光和镜头光晕的算法细节参考了 OpenAI GPT-6 Astra 发布页的实现(下文简称「原站」),并在此基础上加了形状来源、性能自适应和中英文案。
在线演示:主页 · 实验室 · 纯效果页 · 星系体数字 2026 · 用 skill 做出的发布页 · 设计系统组件页 · skill 页面骨架
三种用法,按你要的东西选一个:
1. 让 coding agent 做一整页(推荐)
# Claude Code / Codex / Cursor / Copilot 等:一条命令装进当前项目(加 -g 装到用户级)
npx skills add Win-Hao/starflow
# Open Design
od plugin install github:Win-Hao/starflow@main/skill然后在 Claude Code 里输入 /starflow,或直接对 agent 说一句需求,例如「用 Astra 风格给我们的新模型 Nova 2 做一个发布页:星系首屏、三段故事,星星先聚成光标、再聚成我们的 logo,加一张跑分图表」。它会照抄引擎和首屏骨架、填入你的文案、按设计系统拼出组件。细节见 Skill 与设计系统。
2. 只把星空效果嵌进自己的页面
把 lib/starflow.js 复制到页面旁边(单文件,已打包 three):
<canvas id="sky"></canvas>
<script type="module">
import { createAstraScene } from './starflow.js'
const astra = createAstraScene(document.querySelector('#sky'), { autoRotate: true })
astra.setSource({ type: 'galaxy' }) // 或 { type: 'galaxy-text', value: '2026' }、{ type: 'text', value: '6' }、{ type: 'paths', … }
</script>不想写代码就 <iframe src="embed.html?shape=cursor">。API 见 作为库使用。
3. 本地跑起来看效果、改引擎
git clone https://github.com/Win-Hao/starflow && cd starflow
npm install
npm run dev # http://127.0.0.1:5173 主页 / lab.html 实验室 / embed.html 纯效果
# http://127.0.0.1:5173/examples/launch-page/ 用 skill 做出来的成品发布页只想拿一份设计规范:把 design-systems/openai-astra/DESIGN.md 放进任何项目根目录即可。
| 路径 | 内容 |
|---|---|
/ |
主页:原站首页的滚动编排。右上角可切换中 / 英文,「调节粒子」打开调节面板 |
/lab.html |
实验室:把星星摆成任意文字、内置图标、粘贴的 SVG 或上传的图片,所有参数可调。图标默认走「原站 icon」模式:星场保持星系,用发布页那条滚动形状管线把星汇聚成图标,和发布页里的光标 / 心形完全同一套代码 |
/embed.html |
无 UI 的纯效果页,可直接 <iframe> 嵌入。?shape=cursor / ?shape=openai-knot / ?digits=2026 / ?text=6 / ?icon=heart 切换形状 |
npm install
npm run dev # http://127.0.0.1:5173
npm run build # 三个页面都打进 dist/主页把原站 converge-tilt 预设的整页编排搬了过来,四个阶段用的是同一批星星。
| 阶段 | 驱动 | 规则 |
|---|---|---|
| 翻转 | 首屏之后第一段文案的位置 | 星系绕 X 轴翻到 −52°,翻转进度 75% 处最大、100% 回平 |
| 两侧星轨 | 同上,文案中线到视口中线时散开完成 | 72% 的星被推到文案栏两侧,内缘用 sqrt 稀释;亮度降到 18%、暗星缩到 45%;随滚动有按深度的视差 |
| 光标 | [data-astra-shape] cue 元素(80vh 高、最宽 576px) |
进入视口 0–36% 处成形、50–86% 处消散;星星按种子落到路径上,带着自己在星系里的横向偏移和流速 |
| 结 | 最后一个 cue 常驻 | 6 段弧线各占总长的 1/6,流速按占比放大;成形时自动摆 0.42 rad |
| 翻转 | 两侧星轨 |
|---|---|
![]() |
![]() |
| 光标 | 结 |
|---|---|
![]() |
![]() |
页面只量 DOM、算进度(src/home.js),阻尼、混合、光晕追踪都在引擎里(setScroll):
astra.setScroll({
progress, // scrollY / 800
tiltProgress, // 0..1,null = 跟随 progress
scatterProgress, // 0..1,null = 跟随 progress
contentBounds, // { left, right },视口比例,星轨给它让位
shape: { id, samples, strength, centerNdc, sizeNdc }, // samples 来自 createShapeSamples()
})面板分四组:星场(星数、大小、星带宽度、背景星、厚度、调色板,改动会重建星场)、光学(bloom、亮度、 镜头光晕、氛围色、暗角)、动态(流速、闪烁、形状星带、推斥、星系核自转)、性能(后处理档位、自动降级)。
主页和实验室的文案都有中英两套(src/i18n.js),元素上写 data-i18n="键" 即可;默认按浏览器语言,切换后记在 localStorage。上面的截图是英文界面,下面是中文:
原站的形状只有手绘曲线;实验室把形状来源扩展成三种,最后都变回「可以按弧长参数化的曲线」——
orbitProgress 这个一维弧长参数是整套动画(流动、渐隐、汇聚、光晕追踪)的地基。
形状来源
├─ galaxy paths.js 原站 5 条曲线 → THREE.Curve(含 Z 向起伏)
├─ paths paths.js 任意一组 SVG 路径,每条子路径一层(光标 / OpenAI 结)
├─ galaxy-text digits.js 0–9 每位 5 条手排的螺旋臂 + 核心,写法同原站的 6;多位横向排开
├─ icon 汇聚 paths.js createShapeSamplesFromPolylines:图标抠出的轮廓 / 中线 → 1024 个形状采样点 → setScroll({ shape }),星系的星汇聚成形(发布页光标 / 心形的管线)
└─ text/svg/img rasterize.js → skeleton.js 光栅化 → 距离变换 + Zhang–Suen 细化取笔画中线,星星沿中线撒成管子(文字、描边图标;原站数字的做法)
→ contours.js 实心图标 / 图片:marching squares 抠闭合轮廓 → 等距重采样
↓
field.js 沿曲线撒星(原站逐星公式)+ 每层背景星 + 星系核 → BufferGeometry + 路径贴图
shaders.js 顶点着色器:路径位置 → 星轨 → 路径形状 → 入场汇聚;片元:圆盘 + 十字衍射 + 亚像素解析覆盖
scene.js 正交相机 + 原站同款 Bloom + 镜头光晕 + 氛围 / 暗角 + ACES;滚动状态机;主星追踪
motion.js 指针推斥的 GPGPU ping-pong 模拟
import { createAstraScene } from './astra/index.js'
const astra = createAstraScene(document.querySelector('canvas'))
astra.setSource({ type: 'galaxy' }) // 原站星系
astra.setSource({ type: 'paths', paths: ['M… C…'], viewBox: [0, 0, 19, 19] }) // 一组 SVG 路径
astra.setSource({ type: 'galaxy-text', value: '2026' }) // 星系体数字:每位 5 条螺旋臂 + 一个星系核,原站那个 6 的写法
astra.setSource({ type: 'text', value: '6', fontWeight: 700 }) // 任意文字:沿笔画中线撒成星管
astra.setSource({ type: 'svg', markup: '<svg viewBox="0 0 24 24">…</svg>' })
astra.setSource({ type: 'image', image: htmlImageElement, useLuminance: true })
astra.setConfig({ flowSpeed: 0, bloomIntensity: 0.9 })
astra.setDisperse(1) // 散成两侧星轨,0 是聚回形状
astra.replay() // 重播入场
astra.dispose()| 位置 | 做什么 |
|---|---|
GALAXY_PATHS / GALAXY_LAYERS (paths.js) |
5 条手绘曲线 + 每层的深度 / 相位 / 流速 / 强弱。星数按「强 220 : 弱 170」分配,与弧长无关 |
| 逐星公式 (field.js) | 星带宽 = scatter × lerp(0.3, 1, sin πt);亮星比例强层 8.5% / 弱层 5.5%,端点处再打两折;RNG 种子与原站相同,所以每一颗星的位置都一样 |
| 星系核 (field.js) | 96 颗,半径 r^2.4 × 0.42 的椭圆分布,越靠中心越亮越白,自转 0.36 × flowSpeed rad/s |
| 背景星 | 每层额外 12/88 的星留在天上,浮现进度封顶 0.2 → 永远只有 45% 尺寸;挂在未旋转的根节点下,拖拽时天空不动 |
astraFilteredCore (shaders.js) |
亚像素点的解析覆盖率(三次 B 样条核)。没有它,0.5px 的远星移动时会剧烈闪烁 |
| 尺寸包络 / 端点渐隐 | sizeEnvelope = mix(1, 0.14 + 0.86·sin(πt)^0.68, 0.45),tipFade = smoothstep(0, .055, t) × (1 − smoothstep(.945, 1, t))。流动时星星从端点消失、另一端出现 |
astraIntroMotion |
入场汇聚。每颗星起跑时间和公转角度都不同,形状是「凝聚」而非整体平移 |
astraCoast (shaders.js) |
推斥后的闭式解析外推。松开鼠标后一帧模拟都不用跑 |
| 光晕跟随推斥 (lensflare.js) | 星星被推开的位移只存在 GPU 状态贴图里,所以把主星的 texel 坐标和质量传给光晕着色器,在顶点阶段用同一份 astraCoast 读出偏移 |
| 路径形状 (shaders.js) | 每条子路径当开放曲线;暗星缩小、一半亮星压到 42%;没有主光晕,5 颗主星散在各段路径上带副光晕;变形时带着星系里的横向偏移和流速,5 档流速混在同一条路径上 |
AstraBloomEffect (bloom.js) |
亮度提取前先 2×2 盒采样;原站用两个额外 pass 做高斯重建,这里折进合成着色器,效果等价、零额外 pass |
| 调色板 5 档量化 (palette.js) | 36% 青 / 16% 蓝 / 12% 橙 / 10% 浅橙 / 26% 白。固定比例是「冷底 + 一成暖」观感的来源 |
| 分层旋转滞后 (scene.js) | 拖拽时每条星臂的阻尼 = 14 / (1 + (0.18 + 0.17i) × 0.68 × 2.5),内圈跟手、外圈慢半拍 |
- 色彩管线:全程线性,ACES 只在后处理最后做(
renderer.toneMapping = NoToneMapping,material.toneMapped = false,frameBufferType = HalfFloatType)。顺序搞反,bloom 会提前削顶,画面发灰。 - 星星大小是 2.05,不是 1:亮星要画到 15px 上下再交给 bloom,才是原站那种「大而软」的光斑。
- 辉光来自极低的阈值(0.08),不是大半径。
- 曲线必须是开放的:闭合轮廓没有端点,就没有渐隐和尺寸衰减,星臂会变成一根均匀的项链。
实测(Apple M4,ANGLE/Metal):整帧 GPU 约 6 ms,星星本身只占 0.5 ms,其余全是后处理—— 每个全分辨率 pass 约 2 ms,小 pass 也有 0.15 ms 左右的固定开销。所以优化思路是减 pass,而不是减星星:
- Bloom 的高斯重建从两个 pass 折进合成着色器;镜头光晕着色器对远离光源的像素提前退出
- 氛围色和暗角从 CSS 图层搬进后处理:带
mix-blend-mode的全屏图层会让合成器每帧多画几遍整屏,5K 窗口上占一半掉帧 - 指针推斥的模拟着色器在建场时预编译,第一次划过画布不再卡一下
- 自适应降级:1.2 秒窗口里两成以上的帧慢于阈值就降一级。先降渲染节奏(每 2 / 3 / 4 个刷新周期渲染一次), 节奏到底了才逐级收紧像素预算。大窗口 / 高刷屏上的瓶颈常在浏览器的合成与送显,降分辨率毫无帮助—— 实测连 300×300 的 2D canvas 每帧变色都只能跑到 48fps
quality: 'lite' | 'none'给低端设备;pixelBudget可直接指定
| 参数 | 默认(原站) | 说明 |
|---|---|---|
starCount |
4000 | 路径星总数,按层权重分配 |
backgroundRatio |
0.14 | 留在天上的背景星,相对路径星的比例 |
scatter |
0.041 | 星带半宽,相对形状高度(原站 0.4 / 9.7) |
densityFalloff |
0.22 | 沿路径的疏密调制 |
rotationDepth |
1.4 | 曲线的 Z 向起伏,旋转时星臂的前后层次 |
stroke |
auto |
光栅形状的星线:center 沿笔画中线撒成圆管(原站数字的做法),outline 沿轮廓;auto = 文字和细描边图标走中线,实心图标 / 图片走轮廓 |
strokeSpread |
1.3 | 中线模式的散布,相对当前位置半笔宽的倍率;横向和 Z 向一样宽,旋转到任何角度都是一根管子 |
pathShape |
false | 光栅形状按原站「路径形状」的规矩撒星:横向偏移从星系带来、五颗主星、一半亮星压暗,就是发布页里光标 / 心形那种厚实星团。实验室的图标 / SVG / 图片默认开,配合 scatter: 0.07 |
depth |
自动 | 轮廓模式的体积:每颗星沿 Z 向再撒开的半宽(相对形状高度),默认 0.1;中线模式本身是圆管,默认 0;星系与路径形状默认 0 |
flowInward |
true | 流向星系核;false 向外 |
size |
2.05 | 星星整体大小 |
centerCluster / clusterCount |
true / 96 | 星系核(仅星系模式) |
fillRatio |
0 | 光栅形状内部的星尘占比(仅文字 / 图标) |
brightRetention |
1 | 亮星里保留多少不被压暗到 42%。路径形状预设用 0.5 |
palette |
astra |
astra / aurora / ember / ice / gold |
seed |
0 | 0 = 与原站逐星一致 |
| 参数 | 默认(原站) | 说明 |
|---|---|---|
bloomIntensity / bloomThreshold |
0.7 / 0.08 | |
intensity |
1.35 | 星星整体亮度 |
flowSpeed |
0.8 | 沿星臂流动的速度,0 = 静止 |
sizeFalloff |
0.45 | 端点处星星缩小的程度 |
dimSizeScale |
1 | 暗星的尺寸倍率。路径形状预设用 0.8 |
coreSpin |
true | 星系核自转 |
rotationLag |
0.68 | 拖拽时各层的滞后,0 = 刚性旋转 |
twinkleSpeed |
0.62 | 闪烁 |
introDuration |
5.5 | 入场汇聚时长(秒) |
pointerRepel |
true | GPGPU 指针推斥,半径 176px |
lensMode |
false | 指针透镜放大 |
fillX / fillY |
0.8 / 0.89 | 形状占视口的比例(原站:9.7 / 10.9) |
center |
[0, 0] |
形状偏移,单位是半个视口 |
lensFlare |
原站默认 | intensity .28 / halo .12 / streaks .18 / secondary .55 / ghosts .1 |
ambientColor / ambientOpacity / vignette |
#23435f / 0.55 / 1 |
氛围色与暗角 |
ambientFloor |
0 | 氛围色里铺满整屏的比例,0 = 纯径向渐变。发布页骨架不用它(0),改用 vignette: 0.85,四角和中边的底色按原站 CSS 层实测对齐 |
scrollEffects / scrollStarDriftSpeed |
true / 3 | 滚动编排开关、星轨视差速度 |
shapeAutoRotate / shapeScatter / shapeBrightRetention |
true / 1 / 0.5 | 路径形状的摆动、星带宽度、亮星保留 |
quality |
full |
full(bloom + 光晕)/ lite(只 bloom)/ none(只 ACES) |
pixelBudget / adaptiveQuality |
2.4e6 / true | 像素预算与自动降级 |
- 无 WebGL →
renderStaticFallback()用 Canvas 2D 画同一份星场数据,构图和配色保住 prefers-reduced-motion→ 冻结时间轴(闪烁、流动、自转、推斥全停),形状和 bloom 保留- 无
EXT_color_buffer_float→ 自动关闭 GPGPU 推斥,其余不受影响
- 轨道星尘(orbitalDust):原站有这套代码但默认关闭
- 程序化脏玻璃贴图:原站会运行时生成污渍图做 UV 微畸变,这里只保留胶片颗粒
- GPU 分级:原站用 detect-gpu 分 4 档,这里换成运行时的自适应降级
引擎可以脱离这三个页面单独使用。npm run build:lib 产出三个文件:
| 文件 | 内容 | 用法 |
|---|---|---|
lib/starflow.js |
ES 模块,自带 three + postprocessing(约 650 KB,gzip 165 KB) | 复制到任意页面,<script type="module"> 里 import { createAstraScene } from './starflow.js' |
lib/starflow.iife.js |
同上,挂成全局 window.Starflow |
不用模块的页面 |
lib/starflow.slim.js |
不带依赖 | npm i @win-hao/starflow 后 import { createAstraScene } from '@win-hao/starflow',three / postprocessing 由 npm 解析 |
import { createAstraScene, detectWebGL, renderStaticFallback } from './starflow.js'
const canvas = document.querySelector('canvas')
if (detectWebGL()) {
const astra = createAstraScene(canvas, { autoRotate: true })
astra.setSource({ type: 'galaxy' }) // 或 { type: 'galaxy-text' | 'paths' | 'text' | 'svg' | 'image', … }
// 滚动编排:每帧把进度喂进来,见 src/home.js
// astra.setScroll({ progress, tiltProgress, scatterProgress, contentBounds, shape })
} else {
renderStaticFallback(canvas, { type: 'galaxy' })
}想让 coding agent 直接做出整页的滚动编排,用仓库里的 skill,见下一节。
仓库同时带一个给 coding agent 用的 skill 和一份 DESIGN.md 设计系统,三者共用同一份引擎:
| 目录 | 内容 | 用法 |
|---|---|---|
skill/ |
starflow:SKILL.md、接好线的发布页骨架、纯首屏页、引擎单文件、参考文档 |
Open Design:od plugin install github:Win-Hao/starflow@main/skill;Claude Code / Codex / Cursor:npx skills add Win-Hao/starflow(或手动把 skill/ 拷进 skills 目录),然后说「用 starflow 做一个发布页」 |
design-systems/openai-astra/ |
从 GPT-6 Astra 发布页公开 CSS 提炼的暗色设计系统(DESIGN.md、tokens.css、组件页、preview),按 Open Design 项目规范打包,已投 nexu-io/open-design#7806 | 单独把 DESIGN.md 丢进任何项目根目录,agent 就会按这套风格生成界面 |
docs/upstream-prs.md |
往上游目录投稿的步骤与 PR 文案 |
一句话需求就够,例如:
用 Astra 风格给我们的新模型 Nova 2 做一个发布页:星系首屏、三段故事,星星先聚成光标、再聚成我们的 logo,加一张跑分图表。
agent 读完 SKILL.md 后分三层干活:
| 层 | 内容 | 谁定 |
|---|---|---|
| 照抄 | 引擎单文件、首屏骨架、header / footer 尺寸、黑底白字胶囊控件(规则禁止重写引擎、改首屏几何) | skill |
| 填空 | 首屏两个标签、标题、导语、2–4 段故事、每个形状的说明、结尾;logo 的 SVG 路径、配色 | 用户需求 |
| 按配方生成 | 页面里需要的组件:站点头部、分段控件(6s 自动轮播、联动图表)、图表卡、下拉选择、下载菜单、引用轮播、媒体框与两栏对比、对比表、幻灯片、chip / 脚注 / logo 条 / 媒体条、footer;尺寸、颜色、动效来自 DESIGN.md §4 / §7,现成标记在 design-systems/openai-astra/components.html |
agent 按规则拼 |
最后按 SKILL.md 第 8 步在 1440 / 390 宽度下过一遍检查清单。examples/launch-page/ 就是这样做出来的一页,中文,包含上面全部组件。
npm run build:lib 会把引擎产物同时写进 lib/ 和 skill/assets/;改了 tokens.css 或 DESIGN.md 后跑 scripts/sync-skill.sh 同步组件页和副本。
index.html / src/home.js / src/home.css 主页(滚动编排 + 调节面板 + i18n)
lab.html / src/lab.js / src/lab.css 实验室
embed.html 嵌入页
src/i18n.js 中英文案
src/presets.js 原站路径数据(光标、OpenAI 结)与图标预设
src/lib.js / vite.lib.config.js 库入口与库构建(lib/starflow*.js)
src/astra/ 引擎:scene / field / shaders / paths / bloom / lensflare / ambient / motion / …
docs/screenshots/ README 用图
skill/ coding agent 用的 skill(SKILL.md、页面骨架、引擎单文件)
design-systems/openai-astra/ openai-astra DESIGN.md 设计系统包
docs/upstream-prs.md 往上游目录投稿的步骤与 PR 文案
scripts/sync-skill.sh 重建组件页、同步 DESIGN.md 副本
examples/launch-page/ 中文发布页成品(演示视频用的那页,带录屏自动滚动参数)
参考页面的抓包文件和分析笔记不在仓库里;星星相关的算法都已按上表在源码里注明出处。
MIT。粒子效果的算法参考自 OpenAI GPT-6 Astra 发布页;代码为本项目独立实现。







