# 网页粒子系统 · 阶段二 > 一个纯前端的 **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` 由 Stage 维护,每帧 render。 ### 3.2 坐标系(关键) 编辑器统一采用**数学坐标系:Y 向上为正、向下为负**。Pixi 内部是屏幕坐标系(Y 向下为正),因此: - 发射点/黄点位置:`cy = -centerY`; - 发射器容器 `em` position:`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)。 --- ## 四、踩坑点(重要) 1. **签名 watch 粒度** - 起初 `watch(store.systems.map(config), {deep:true})` 监听整个 config;但发射器每帧改 `_elapsed` 等内部字段 → 每帧触发 → 每帧重算 totalFrames + 清空重录 → **卡死**。 - 修复:`JSON.stringify(s.config, 排除 texture)` 生成**签名字符串**比较,只有"影响渲染的参数"变化才触发;并把 `_elapsed/_delayLeft` 从 config 移至 emitter **私有字段** `_elapsed`,避免污染响应式。 2. **坐标轴文字放大变糊** - Text 放 world 容器随 viewScale 放大变为位图模糊。 - 修复:文字放到**独立 `axisLabelLayer`(不随 world 缩放)**,用世界→屏幕坐标定位;字号由"系统设置"手动控制。 3. **曲线编辑器椭圆节点/选不中** - SVG `viewBox + preserveAspectRatio="none"` 把圆拉伸成椭圆、命中区过小。 - 修复:改按实际像素绘制(ResizeObserver 量尺寸),节点为正圆,叠加大命中圈,Pointer 捕获拖动。 4. **曲线节点双击删除又新建** - dblclick 冒泡到 SVG 触发"双击空白新建"。 - 修复:删除节点记录时间戳,`onDbl`(新建)在删除后 400ms 内忽略(抑制窗口)。 5. **Y 轴方向反** - 坐标轴标签用数学坐标(上正),发射器/黄点用屏幕坐标(下正),方向相反。 - 修复:黄点/发射器容器 position 的 y 取负,统一到数学坐标系。 6. **锥体朝向与粒子方向相反** - 粒子发射用 `-sin`(数学上正),形状范围绘制用 `+sin`,二者符号不一致。 - 修复:drawShapeRange 逐点做 `pt` 变换时统一 `Y = -ly - oyo`(与粒子一致)。 7. **锥体粒子散布在扇区内 vs 从中心发射** - 初版把粒子位置散布在扇形区域(半径 × coneAngle),与圆的"中心全向"不同。 - 按用户意图修正:粒子**从锥形顶点(中心)发射,方向限 `direction ± coneAngle/2` 扇形**,类似"圆心向各方向,但只限锥体弧度范围"。 8. **偏移不影响形状范围** - 形状范围中心画在 root 中心,没用 offset;粒子发射源 = root 中心 + 偏移,导致偏移时粒子相对形状范围偏移。 - 修复:drawShapeRange 的 `pt` 把偏移 `(oxo, oyo)` 并入局部原点;且 `offsetY` 转屏幕取负(`Y = -ly - oyo`)。 9. **发射时长 1 秒后停发** - `duration` 默认 1.0,爆发/持续 1 秒后 `withinDuration=false` 停发,粒子随后消亡 → "没粒子"。 - 修复:配合 UI 移除"发射时长/持续爆发",`duration` 默认改 0(=无限),`burstLoop` 默认 false(一次性爆发)。 10. **gizmo 拖拽不跟手** - 位移用 `cfg.centerX + dx`(基于当前值累加总位移 dx),鼠标一动重复叠加 → 越拖越偏。 - 修复:存按下时基准 `sCx/sCy`,位移 = 基准 + 总位移;旋转/缩放同样基于基准快照,并去掉 `Math.round` 取整抖动。 11. **默认贴图/命名等默认值** - 曾把默认 textureName 设为 dot、并在用户改图后仍用旧缓存;后按用户要求默认 `star.png`,硬刷新即可。 - 系统命名:默认英文 `ParticleSystem1`,同名自动 +1(正则 `^ParticleSystem(\d+)$` 取最大值 +1)。 12. **新增系统不显示贴图** - 初期只给 onMounted 时存在的系统赋 texture,后续 `addSystem` 新增系统 texture 为 null → Sprite 画不出。 - 修复:在 `syncEmitters()`(每帧)里,给 `dotTex 已加载但 sys.config.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_`、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 取负` 的数学坐标约定。