14 KiB
网页粒子系统 · 阶段二
一个纯前端的 Spine 粒子发射编辑器(参考 喵喵 Spine Particle / Spine Particle Studio)。 本阶段聚焦「粒子发射编辑器的系统设计与显示」,不强行绑定 Spine 运行时;Spine 仅是未来导出格式的参考,
spine/sample.json只作素材,不做为编辑器模板。 工程目录:/Users/tianmokeji/Desktop/SpineParticle
一、项目定位与技术栈
定位:可视化配置粒子特效、实时预览,最终输出导出所需的数据模型(导出为后续阶段,现阶段为预览/编辑铺路)。
技术栈
- 前端框架:Vue 3 + TypeScript + Vite
- 状态管理:Pinia
- 渲染:PixiJS 7.4(
@esotericsoftware/spine-pixi-v7已卸载,当前不依赖任何 Spine 运行时;pixi.js用于画布) - 粒子:自研粒子发射器
ParticleEmitter(池化,Sprite 池)
明确不做(预留):导出 Spine JSON 烘焙、骨骼对象池匹配 Spine 运行时。(本阶段核心 = 编辑器 + 预览。)
二、已完成功能清单
2.1 面板(ParticlePanel)架构
按参考站分组、全部可折叠:
| 分组 | 内容 |
|---|---|
| 场景对象 | 树形层级(●粒子系统 / ◇轨道路径【预留】 / ◇碰撞体【预留】),名称、位置X、位置Y、偏移X、偏移Y、旋转、缩放X、缩放Y |
| 发射模式 | 持续(发射速率) / 爆发(单次数量) |
| 发射器形状 | 形状(点/圆/矩形/锥体/轨道),圆/锥(半径)、矩形(宽/高)、锥形开口角度、发射角度、扩散角度 |
| 粒子属性 | 速度下限/上限、生命下限/上限、缩放下限/上限、旋转速度、随机种子 |
| 外观与资源 | 透明度(曲线/固定)、曲线模式(跟随导出/线性/贝塞尔)、图片资源(子文件夹/名称)、颜色类型(固定色/渐变)、混合(正常/相加/相乘/滤色)、占比与缩放(占比/缩放/轴心/独立透明度) |
| 修改器 (Modifiers) | 移动噪声、重力、风力、力场、拖尾、碰撞【预留】、路径跟随【预留】、阻尼 |
| 导出设置 | 预留(占位) |
数值调节统一为滑块 + 数字框并存(NumSlider 组件)。
2.2 画布(Stage)
- 网格背景(可切 ▦),深色棋盘格。
- 滚轮缩放画布:
默认 100%,最小 1%,无上限;以鼠标为中心缩放(鼠标下的世界点不漂移)。 - 左键长按拖动画布(平移)。
- 顶部工具栏:
- 根变换工具:✋ 拖动画布(默认)、✥ 位移、⟳ 旋转、⤢ 缩放(切换后用 gizmo 拖拽根节点)。
- 视图工具:+/- 缩放、⤢ 适配画布、▦ 网格、⟳ 重置视图。
- 开关:发射点、骨骼连线、⚙ 系统设置。
- 坐标轴:
center+center无限长 x/y 轴 + 自适应整数刻度,数字标签放独立不缩放图层(避免放大变糊);由系统设置"启用刻度"主开关控制。 - 发射点(黄点):每个系统一个,绑 位置X/位置Y(centerX/centerY);选中高亮黄,其余淡蓝灰;无系统不画。
- 发射器形状范围:选中对应形状时绘制绿圆(圆形)、蓝矩形(矩形)、绿锥形(锥体),随参数实时响应;点/轨道不绘制。
- 根变换 gizmo:选中工具后在 root 中心亮青色手柄(位移十字箭头 / 旋转圆环 / 缩放方框角点)。
2.3 时间轴(Timeline)
- 控制按钮:❚❚/▶ 播放暂停、⏮ 回到起点、⟳ 循环。
- 帧显示
当前帧 / 总帧数,动画名下拉。 - 标尺:整数帧刻度;滚轮缩放只改变显示范围(视口帧数,左端恒为第 0 帧),不改变播放时长;刻度为整数、最小 0。
- 每系统一条轨道;播放头可拖动滑动。
- 粒子条:位置 = 延迟(delay)起点,宽度 = 发射时长(duration)+粒子生命(lifeMax) = 总时长;左端之前空白 = 延迟时长。可整体拖动(调 delay)、拖右缘(调 duration)。
2.4 系统设置(⚙ 面板)
- 启用刻度 主开关(默认关闭):开启才显示坐标轴并展开其余配置项。
- 刻度文字大小(默认 12px)、刻度线粗细、坐标轴颜色、显示刻度数值(均随"启用刻度"显隐)。
2.5 默认值(按用户给定的截图定版)
| 项 | 默认 |
|---|---|
| 发射模式 | 持续,发射速率 = 20 |
| 爆发 | 单次数量 = 50 |
| 延迟时间 | 0 |
| 发射时长 | 0(= 无限,已在 UI 移除该项) |
| 持续爆发开关 | 已移除 |
| 形状 | 点;发射角度 0、扩散角度 360 |
| 圆形 | 半径 100,方向 -90,扩散 0 |
| 矩形 | 宽 200,高 100,方向 -90,扩散 0 |
| 锥体 | 半径 100,锥形开口 90,朝向 90 |
| 系统命名 | ParticleSystem1(英文,同名自动 +1) |
| 贴图 | star.png |
| 透明度 | 曲线模式,默认点 (0,0)(0.2,1)(0.8,1)(1,0) |
| 随机种子 | 0(每次不同) |
三、核心架构设计
3.1 数据流(单向)
ParticlePanel(Template) ⇄ Pinia Store (systems[]) ⇄ Stage.loop (emitterMap)
▲ │
└────── 面板 v-model 改 config ──────┘
sys.config按引用保持:面板直接改 config 字段,Pixi 发射器读到的是同一对象,改动实时生效。emitterMap: Map<systemId, ParticleEmitter>由 Stage 维护,每帧 render。
3.2 坐标系(关键)
编辑器统一采用数学坐标系:Y 向上为正、向下为负。Pixi 内部是屏幕坐标系(Y 向下为正),因此:
- 发射点/黄点位置:
cy = -centerY; - 发射器容器
emposition:em.position.set(centerX, -centerY); - 初始速度 y:
vy = -sin(ang)(锥体); - 形状范围、gizmo 绘制均遵循
cy 取负。
统一手法:显示层(黄点、形状、gizmo、容器 position)都做 Y 取负转换;而粒子物理/运动方向保持屏幕坐标,二者在"发射方向(锥体)"这一处需显式对齐。
3.3 发射器(ParticleEmitter extends Container)
粒子是发射器容器的子项(Sprite 池),坐标在发射器局部坐标。因此对容器设 position/rotation/scale 即整体变换整簇粒子 = "root 变换"。
角色划分:
- root 位移 =
centerX/centerY→ 容器position; - root 旋转 =
rootRotation→ 容器rotation(数学上正,顺时针为负); - root 缩放 =
rootScaleX/rootScaleY→ 容器scale。
发射局部原点:粒子发射位置 = 中心(centerX) + 偏移(offsetX/offsetY),其中 spawnOrigin 的局部原点只含 offset,中心由容器 position 补回。这样旋转/缩放绕 root 中心,且最终世界坐标不变。
3.4 发射模式与形状
- 持续(stream):
accumulator += dt * rate,累积满 1 发射一个。 - 爆发(burst):一次性
emitBurst()发burstCount个。 - 发射形状(
spawnOrigin):- point:中心;
- circle:半径内圆散布;
- rect:宽高矩形散布;
- cone:从锥形顶点(中心)发射,方向在
direction ± coneAngle/2扇形内随机、半径 0~radius(与圆不同,只限锥体弧度范围)。
- 速度方向(
spawnOne):锥体用 coneAngle 散布 +vy=-sin;其余用 spread + 屏幕坐标。
3.5 根变换 gizmo
- 3 种工具:位移 / 旋转 / 缩放,切换后画布按住拖动改
config.centerX/centerY、rootRotation、rootScaleX/ScaleY。 - 与场景对象面板参数双向联动。
- 计算基于按下时的快照 + 相对按下点的总增量(位移/旋转/缩放都不累积),保证"跟手"。
3.6 录制 → 回放
- Stage.loop 每帧
em.update(dt)(固定dt = 1/fps保证帧对齐),产生的ParticleState[]深拷贝存到sys.frames[frame]。 - 录完后进入回放:
em.apply(frames[frame]),到尾帧若 loop 则回 0,否则停末帧。 - 每帧也收集活动粒子状态
collected(供 HUD 数字化/骨骼连线)。
3.7 时间轴总帧数(自动计算)
totalFrames = ceil( max(对所有系统 (delay + duration + lifeMax)) * fps )。- 即"最后一个粒子消失的时刻";改 delay/duration/lifeMax 时重算。
- 通过 字段签名 watch 触发重录(见踩坑点 #1)。
四、踩坑点(重要)
-
签名 watch 粒度
- 起初
watch(store.systems.map(config), {deep:true})监听整个 config;但发射器每帧改_elapsed等内部字段 → 每帧触发 → 每帧重算 totalFrames + 清空重录 → 卡死。 - 修复:
JSON.stringify(s.config, 排除 texture)生成签名字符串比较,只有"影响渲染的参数"变化才触发;并把_elapsed/_delayLeft从 config 移至 emitter 私有字段_elapsed,避免污染响应式。
- 起初
-
坐标轴文字放大变糊
- Text 放 world 容器随 viewScale 放大变为位图模糊。
- 修复:文字放到独立
axisLabelLayer(不随 world 缩放),用世界→屏幕坐标定位;字号由"系统设置"手动控制。
-
曲线编辑器椭圆节点/选不中
- SVG
viewBox + preserveAspectRatio="none"把圆拉伸成椭圆、命中区过小。 - 修复:改按实际像素绘制(ResizeObserver 量尺寸),节点为正圆,叠加大命中圈,Pointer 捕获拖动。
- SVG
-
曲线节点双击删除又新建
- dblclick 冒泡到 SVG 触发"双击空白新建"。
- 修复:删除节点记录时间戳,
onDbl(新建)在删除后 400ms 内忽略(抑制窗口)。
-
Y 轴方向反
- 坐标轴标签用数学坐标(上正),发射器/黄点用屏幕坐标(下正),方向相反。
- 修复:黄点/发射器容器 position 的 y 取负,统一到数学坐标系。
-
锥体朝向与粒子方向相反
- 粒子发射用
-sin(数学上正),形状范围绘制用+sin,二者符号不一致。 - 修复:drawShapeRange 逐点做
pt变换时统一Y = -ly - oyo(与粒子一致)。
- 粒子发射用
-
锥体粒子散布在扇区内 vs 从中心发射
- 初版把粒子位置散布在扇形区域(半径 × coneAngle),与圆的"中心全向"不同。
- 按用户意图修正:粒子从锥形顶点(中心)发射,方向限
direction ± coneAngle/2扇形,类似"圆心向各方向,但只限锥体弧度范围"。
-
偏移不影响形状范围
- 形状范围中心画在 root 中心,没用 offset;粒子发射源 = root 中心 + 偏移,导致偏移时粒子相对形状范围偏移。
- 修复:drawShapeRange 的
pt把偏移(oxo, oyo)并入局部原点;且offsetY转屏幕取负(Y = -ly - oyo)。
-
发射时长 1 秒后停发
duration默认 1.0,爆发/持续 1 秒后withinDuration=false停发,粒子随后消亡 → "没粒子"。- 修复:配合 UI 移除"发射时长/持续爆发",
duration默认改 0(=无限),burstLoop默认 false(一次性爆发)。
-
gizmo 拖拽不跟手
- 位移用
cfg.centerX + dx(基于当前值累加总位移 dx),鼠标一动重复叠加 → 越拖越偏。 - 修复:存按下时基准
sCx/sCy,位移 = 基准 + 总位移;旋转/缩放同样基于基准快照,并去掉Math.round取整抖动。
- 位移用
-
默认贴图/命名等默认值
- 曾把默认 textureName 设为 dot、并在用户改图后仍用旧缓存;后按用户要求默认
star.png,硬刷新即可。 - 系统命名:默认英文
ParticleSystem1,同名自动 +1(正则^ParticleSystem(\d+)$取最大值 +1)。
- 曾把默认 textureName 设为 dot、并在用户改图后仍用旧缓存;后按用户要求默认
-
新增系统不显示贴图
- 初期只给 onMounted 时存在的系统赋 texture,后续
addSystem新增系统 texture 为 null → Sprite 画不出。 - 修复:在
syncEmitters()(每帧)里,给dotTex 已加载但 sys.config.texture 为空的系统补上默认纹理。
- 初期只给 onMounted 时存在的系统赋 texture,后续
五、根节点变换的坐标正确性(易错综述)
- 发射器容器(position/rotation/scale)承载 root 变换。
- 必须把发射局部原点改为"只含 offset"(去 centerX),centerX 由容器 position 补回;否则 position 与粒子局部坐标双重叠加 → 位移重复。
- 旋转/缩放绕 root 中心成立,前提是"root 中心 = 容器局部原点(0,0)"。
- 数学坐标系下,一切显示层 y 取负;粒子运动方向(y 向)在锥体方向处需与形状绘制对齐。
六、响应式与性能要点
sys.config保持引用,面板 v-model 直接改属字段 → 实时生效,无深拷贝/重建。- 避免 deep watch 整个 config;用签名字符串监听 + 内部计时字段移出 config。
- 录制固定步长
dt = 1/fps,保证回放与实时帧对齐、可复现。 - 确定性随机(种子):
mulberry32;seed>0时可复现;仅在种子变化时重建 RNG,reset()时若种子>0 重置到序列头,保证同种子从头录制结果一致。
七、文件结构
src/
main.ts # 入口,注册 Pinia
App.vue # 布局:左面板 / 右上画布 / 右下时间轴(宽度/高度可拖拽)
style.css # 全局样式(深色主题)
core/
particleEmitter.ts # 发射器:config + defaultConfig + 池化更新/捕获/回放/重置/确定性随机
store/
particleStore.ts # Pinia:systems[] + activeId + timeline + settings + recalcTotalFrames
views/
Stage.vue # 画布:world/粒子/坐标轴/黄点/形状范围/gizmo + 录制回放 + 根变换
ParticlePanel.vue # 参数面板(分组折叠 + NumSlider + 形状默认值切换)
Timeline.vue # 时间轴(控制/标尺缩放/轨道/粒子条拖拽)
CurveEditor.vue # 透明度曲线编辑器(节点可拖/双击删/双击空白新建)
NumSlider.vue # 滑块 + 数字框 双控组件
public/
particles/star.png # 默认粒子贴图(另有 dot.png / spark.png)
spine/sample.json # 仅作素材参考,不作编辑器模板
八、当前遗留 / 预留(后续阶段)
- 导出 Spine JSON 烘焙(用户确认后置):现阶段编辑/预览,导出的数据模型(每粒子一根骨骼
boneName=p_<i>、ParticleState)已为此铺路。 - 轨道路径 / 碰撞体:场景对象中为预留占位(
collision/pathFollow字段已存在)。 - 导出设置:分组占位。
- 修改器实现程度:重力/风力/力场/移动噪声/阻尼已接入
emitter.update;拖尾/碰撞/路径跟随为占位/预留。
九、常见调整指引
- 默认参数集中在本文件 §2.5(来自
particleEmitter.ts的defaultConfig()与面板SHAPE_DEFAULTS)。 - 形状范围跟随 root 旋转/缩放/偏移:改
Stage.vue的drawShapeRange(逐点pt变换)。 - 根变换 gizmo:改
Stage.vue的drawGizmo/onGizmoDown。 - 时间轴总帧:改
store.recalcTotalFrames。 - 坐标系:凡涉及显示 y 方向,遵循
Y 取负的数学坐标约定。