Files
SpineParticlesWeb/Document/网页粒子系统阶段二.md
2026-09-02 10:45:10 +08:00

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;
  • 发射器容器 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/centerYrootRotationrootScaleX/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_<i>、ParticleState)已为此铺路。
  • 轨道路径 / 碰撞体:场景对象中为预留占位(collision/pathFollow 字段已存在)。
  • 导出设置:分组占位。
  • 修改器实现程度:重力/风力/力场/移动噪声/阻尼已接入 emitter.update;拖尾/碰撞/路径跟随为占位/预留。

九、常见调整指引

  • 默认参数集中在本文件 §2.5(来自 particleEmitter.tsdefaultConfig() 与面板 SHAPE_DEFAULTS)。
  • 形状范围跟随 root 旋转/缩放/偏移:改 Stage.vuedrawShapeRange(逐点 pt 变换)。
  • 根变换 gizmo:改 Stage.vuedrawGizmo / onGizmoDown
  • 时间轴总帧:改 store.recalcTotalFrames
  • 坐标系:凡涉及显示 y 方向,遵循 Y 取负 的数学坐标约定。