@nice2dev/game-engine 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/cjs/ai/BehaviorTree.js +1215 -0
  2. package/dist/cjs/ai/BehaviorTree.js.map +1 -0
  3. package/dist/cjs/ai/StateMachine.js +783 -0
  4. package/dist/cjs/ai/StateMachine.js.map +1 -0
  5. package/dist/cjs/editor/ShaderGraph.js +1616 -0
  6. package/dist/cjs/editor/ShaderGraph.js.map +1 -0
  7. package/dist/cjs/editor/TimelineEditor.js +819 -0
  8. package/dist/cjs/editor/TimelineEditor.js.map +1 -0
  9. package/dist/cjs/export/GodotExporter.js +1102 -0
  10. package/dist/cjs/export/GodotExporter.js.map +1 -0
  11. package/dist/cjs/export/PlatformExporter.js +236 -0
  12. package/dist/cjs/export/PlatformExporter.js.map +1 -0
  13. package/dist/cjs/export/ThreeJSExporter.js +1116 -0
  14. package/dist/cjs/export/ThreeJSExporter.js.map +1 -0
  15. package/dist/cjs/export/UnityExporter.js +1193 -0
  16. package/dist/cjs/export/UnityExporter.js.map +1 -0
  17. package/dist/cjs/export/WebExporter.js +1036 -0
  18. package/dist/cjs/export/WebExporter.js.map +1 -0
  19. package/dist/cjs/export/index.js +58 -0
  20. package/dist/cjs/export/index.js.map +1 -0
  21. package/dist/cjs/import/AsepriteImporter.js +761 -0
  22. package/dist/cjs/import/AsepriteImporter.js.map +1 -0
  23. package/dist/cjs/import/DragonBonesImporter.js +499 -0
  24. package/dist/cjs/import/DragonBonesImporter.js.map +1 -0
  25. package/dist/cjs/import/GameMakerImporter.js +559 -0
  26. package/dist/cjs/import/GameMakerImporter.js.map +1 -0
  27. package/dist/cjs/import/GodotSceneImporter.js +824 -0
  28. package/dist/cjs/import/GodotSceneImporter.js.map +1 -0
  29. package/dist/cjs/import/LDtkImporter.js +481 -0
  30. package/dist/cjs/import/LDtkImporter.js.map +1 -0
  31. package/dist/cjs/import/Live2DImporter.js +553 -0
  32. package/dist/cjs/import/Live2DImporter.js.map +1 -0
  33. package/dist/cjs/import/NdgFormat.js +499 -0
  34. package/dist/cjs/import/NdgFormat.js.map +1 -0
  35. package/dist/cjs/import/OgmoImporter.js +529 -0
  36. package/dist/cjs/import/OgmoImporter.js.map +1 -0
  37. package/dist/cjs/import/RPGMakerImporter.js +520 -0
  38. package/dist/cjs/import/RPGMakerImporter.js.map +1 -0
  39. package/dist/cjs/import/SceneImporter.js +449 -0
  40. package/dist/cjs/import/SceneImporter.js.map +1 -0
  41. package/dist/cjs/import/SpineImporter.js +583 -0
  42. package/dist/cjs/import/SpineImporter.js.map +1 -0
  43. package/dist/cjs/import/SpriterImporter.js +652 -0
  44. package/dist/cjs/import/SpriterImporter.js.map +1 -0
  45. package/dist/cjs/import/TiledMapImporter.js +859 -0
  46. package/dist/cjs/import/TiledMapImporter.js.map +1 -0
  47. package/dist/cjs/import/UnitySceneImporter.js +732 -0
  48. package/dist/cjs/import/UnitySceneImporter.js.map +1 -0
  49. package/dist/cjs/import/index.js +305 -0
  50. package/dist/cjs/import/index.js.map +1 -0
  51. package/dist/cjs/index.js +201 -0
  52. package/dist/cjs/index.js.map +1 -1
  53. package/dist/cjs/scripting/GraphToAST.js +567 -0
  54. package/dist/cjs/scripting/GraphToAST.js.map +1 -0
  55. package/dist/cjs/scripting/LanguageExporter.js +321 -0
  56. package/dist/cjs/scripting/LanguageExporter.js.map +1 -0
  57. package/dist/cjs/scripting/ScriptAST.js +67 -0
  58. package/dist/cjs/scripting/ScriptAST.js.map +1 -0
  59. package/dist/cjs/scripting/VisualScripting2.js +1140 -0
  60. package/dist/cjs/scripting/VisualScripting2.js.map +1 -0
  61. package/dist/cjs/scripting/exporters/CSharpExporter.js +503 -0
  62. package/dist/cjs/scripting/exporters/CSharpExporter.js.map +1 -0
  63. package/dist/cjs/scripting/exporters/GDScriptExporter.js +452 -0
  64. package/dist/cjs/scripting/exporters/GDScriptExporter.js.map +1 -0
  65. package/dist/cjs/scripting/exporters/LuaExporter.js +457 -0
  66. package/dist/cjs/scripting/exporters/LuaExporter.js.map +1 -0
  67. package/dist/cjs/scripting/exporters/PythonExporter.js +565 -0
  68. package/dist/cjs/scripting/exporters/PythonExporter.js.map +1 -0
  69. package/dist/cjs/scripting/exporters/RustExporter.js +525 -0
  70. package/dist/cjs/scripting/exporters/RustExporter.js.map +1 -0
  71. package/dist/cjs/scripting/exporters/TypeScriptExporter.js +570 -0
  72. package/dist/cjs/scripting/exporters/TypeScriptExporter.js.map +1 -0
  73. package/dist/cjs/systems/ParticleSystem2.js +1478 -0
  74. package/dist/cjs/systems/ParticleSystem2.js.map +1 -0
  75. package/dist/esm/ai/BehaviorTree.js +1186 -0
  76. package/dist/esm/ai/BehaviorTree.js.map +1 -0
  77. package/dist/esm/ai/StateMachine.js +767 -0
  78. package/dist/esm/ai/StateMachine.js.map +1 -0
  79. package/dist/esm/editor/ShaderGraph.js +1606 -0
  80. package/dist/esm/editor/ShaderGraph.js.map +1 -0
  81. package/dist/esm/editor/TimelineEditor.js +800 -0
  82. package/dist/esm/editor/TimelineEditor.js.map +1 -0
  83. package/dist/esm/export/GodotExporter.js +1100 -0
  84. package/dist/esm/export/GodotExporter.js.map +1 -0
  85. package/dist/esm/export/PlatformExporter.js +230 -0
  86. package/dist/esm/export/PlatformExporter.js.map +1 -0
  87. package/dist/esm/export/ThreeJSExporter.js +1114 -0
  88. package/dist/esm/export/ThreeJSExporter.js.map +1 -0
  89. package/dist/esm/export/UnityExporter.js +1191 -0
  90. package/dist/esm/export/UnityExporter.js.map +1 -0
  91. package/dist/esm/export/WebExporter.js +1033 -0
  92. package/dist/esm/export/WebExporter.js.map +1 -0
  93. package/dist/esm/export/index.js +44 -0
  94. package/dist/esm/export/index.js.map +1 -0
  95. package/dist/esm/import/AsepriteImporter.js +759 -0
  96. package/dist/esm/import/AsepriteImporter.js.map +1 -0
  97. package/dist/esm/import/DragonBonesImporter.js +496 -0
  98. package/dist/esm/import/DragonBonesImporter.js.map +1 -0
  99. package/dist/esm/import/GameMakerImporter.js +556 -0
  100. package/dist/esm/import/GameMakerImporter.js.map +1 -0
  101. package/dist/esm/import/GodotSceneImporter.js +822 -0
  102. package/dist/esm/import/GodotSceneImporter.js.map +1 -0
  103. package/dist/esm/import/LDtkImporter.js +479 -0
  104. package/dist/esm/import/LDtkImporter.js.map +1 -0
  105. package/dist/esm/import/Live2DImporter.js +550 -0
  106. package/dist/esm/import/Live2DImporter.js.map +1 -0
  107. package/dist/esm/import/NdgFormat.js +490 -0
  108. package/dist/esm/import/NdgFormat.js.map +1 -0
  109. package/dist/esm/import/OgmoImporter.js +526 -0
  110. package/dist/esm/import/OgmoImporter.js.map +1 -0
  111. package/dist/esm/import/RPGMakerImporter.js +517 -0
  112. package/dist/esm/import/RPGMakerImporter.js.map +1 -0
  113. package/dist/esm/import/SceneImporter.js +441 -0
  114. package/dist/esm/import/SceneImporter.js.map +1 -0
  115. package/dist/esm/import/SpineImporter.js +580 -0
  116. package/dist/esm/import/SpineImporter.js.map +1 -0
  117. package/dist/esm/import/SpriterImporter.js +649 -0
  118. package/dist/esm/import/SpriterImporter.js.map +1 -0
  119. package/dist/esm/import/TiledMapImporter.js +857 -0
  120. package/dist/esm/import/TiledMapImporter.js.map +1 -0
  121. package/dist/esm/import/UnitySceneImporter.js +730 -0
  122. package/dist/esm/import/UnitySceneImporter.js.map +1 -0
  123. package/dist/esm/import/index.js +279 -0
  124. package/dist/esm/import/index.js.map +1 -0
  125. package/dist/esm/index.js +36 -0
  126. package/dist/esm/index.js.map +1 -1
  127. package/dist/esm/scripting/GraphToAST.js +564 -0
  128. package/dist/esm/scripting/GraphToAST.js.map +1 -0
  129. package/dist/esm/scripting/LanguageExporter.js +311 -0
  130. package/dist/esm/scripting/LanguageExporter.js.map +1 -0
  131. package/dist/esm/scripting/ScriptAST.js +52 -0
  132. package/dist/esm/scripting/ScriptAST.js.map +1 -0
  133. package/dist/esm/scripting/VisualScripting2.js +1130 -0
  134. package/dist/esm/scripting/VisualScripting2.js.map +1 -0
  135. package/dist/esm/scripting/exporters/CSharpExporter.js +501 -0
  136. package/dist/esm/scripting/exporters/CSharpExporter.js.map +1 -0
  137. package/dist/esm/scripting/exporters/GDScriptExporter.js +450 -0
  138. package/dist/esm/scripting/exporters/GDScriptExporter.js.map +1 -0
  139. package/dist/esm/scripting/exporters/LuaExporter.js +455 -0
  140. package/dist/esm/scripting/exporters/LuaExporter.js.map +1 -0
  141. package/dist/esm/scripting/exporters/PythonExporter.js +563 -0
  142. package/dist/esm/scripting/exporters/PythonExporter.js.map +1 -0
  143. package/dist/esm/scripting/exporters/RustExporter.js +523 -0
  144. package/dist/esm/scripting/exporters/RustExporter.js.map +1 -0
  145. package/dist/esm/scripting/exporters/TypeScriptExporter.js +568 -0
  146. package/dist/esm/scripting/exporters/TypeScriptExporter.js.map +1 -0
  147. package/dist/esm/systems/ParticleSystem2.js +1471 -0
  148. package/dist/esm/systems/ParticleSystem2.js.map +1 -0
  149. package/dist/types/ai/BehaviorTree.d.ts +375 -0
  150. package/dist/types/ai/StateMachine.d.ts +296 -0
  151. package/dist/types/editor/ShaderGraph.d.ts +207 -0
  152. package/dist/types/editor/TimelineEditor.d.ts +393 -0
  153. package/dist/types/export/GodotExporter.d.ts +56 -0
  154. package/dist/types/export/PlatformExporter.d.ts +201 -0
  155. package/dist/types/export/ThreeJSExporter.d.ts +40 -0
  156. package/dist/types/export/UnityExporter.d.ts +69 -0
  157. package/dist/types/export/WebExporter.d.ts +58 -0
  158. package/dist/types/export/index.d.ts +19 -0
  159. package/dist/types/import/AsepriteImporter.d.ts +46 -0
  160. package/dist/types/import/DragonBonesImporter.d.ts +331 -0
  161. package/dist/types/import/GameMakerImporter.d.ts +375 -0
  162. package/dist/types/import/GodotSceneImporter.d.ts +34 -0
  163. package/dist/types/import/LDtkImporter.d.ts +177 -0
  164. package/dist/types/import/Live2DImporter.d.ts +237 -0
  165. package/dist/types/import/NdgFormat.d.ts +387 -0
  166. package/dist/types/import/OgmoImporter.d.ts +237 -0
  167. package/dist/types/import/RPGMakerImporter.d.ts +186 -0
  168. package/dist/types/import/SceneImporter.d.ts +276 -0
  169. package/dist/types/import/SpineImporter.d.ts +372 -0
  170. package/dist/types/import/SpriterImporter.d.ts +230 -0
  171. package/dist/types/import/TiledMapImporter.d.ts +57 -0
  172. package/dist/types/import/UnitySceneImporter.d.ts +87 -0
  173. package/dist/types/import/index.d.ts +59 -0
  174. package/dist/types/index.d.ts +29 -17
  175. package/dist/types/scripting/GraphToAST.d.ts +55 -0
  176. package/dist/types/scripting/LanguageExporter.d.ts +136 -0
  177. package/dist/types/scripting/ScriptAST.d.ts +312 -0
  178. package/dist/types/scripting/VisualScripting2.d.ts +353 -0
  179. package/dist/types/scripting/exporters/CSharpExporter.d.ts +44 -0
  180. package/dist/types/scripting/exporters/GDScriptExporter.d.ts +46 -0
  181. package/dist/types/scripting/exporters/LuaExporter.d.ts +46 -0
  182. package/dist/types/scripting/exporters/PythonExporter.d.ts +49 -0
  183. package/dist/types/scripting/exporters/RustExporter.d.ts +46 -0
  184. package/dist/types/scripting/exporters/TypeScriptExporter.d.ts +48 -0
  185. package/dist/types/scripting/exporters/index.d.ts +8 -0
  186. package/dist/types/scripting/index.d.ts +11 -0
  187. package/dist/types/systems/ParticleSystem2.d.ts +646 -0
  188. package/package.json +2 -2
@@ -0,0 +1,1471 @@
1
+ /**
2
+ * @file ParticleSystem2.ts
3
+ * @description Advanced Particle System with GPU Compute and Visual Editor
4
+ * PRO-1.4: Professional Editor Features
5
+ *
6
+ * Features:
7
+ * - GPU-accelerated particles (WebGPU Compute)
8
+ * - Multiple emitter shapes
9
+ * - Force fields (gravity, wind, vortex, etc.)
10
+ * - Sub-emitters and trails
11
+ * - Collision detection
12
+ * - LOD system
13
+ * - Visual editor integration
14
+ */
15
+ // ============================================================
16
+ // Particle System Class
17
+ // ============================================================
18
+ class ParticleSystem2 {
19
+ constructor(def) {
20
+ this.useGPU = false;
21
+ this.definition = def;
22
+ this.state = this.createInitialState();
23
+ }
24
+ createInitialState() {
25
+ const maxParticles = this.definition.main.maxParticles;
26
+ const particles = [];
27
+ for (let i = 0; i < maxParticles; i++) {
28
+ particles.push(this.createDeadParticle());
29
+ }
30
+ return {
31
+ particles,
32
+ activeCount: 0,
33
+ time: 0,
34
+ isPlaying: false,
35
+ isPaused: false,
36
+ isStopped: true,
37
+ isEmitting: false,
38
+ randomSeed: (Math.random() * 0xffffffff) | 0,
39
+ emitCounter: 0,
40
+ distanceTraveled: 0,
41
+ lastPosition: new Float32Array([0, 0, 0]),
42
+ burstStates: this.definition.emission.bursts.map((b, i) => ({
43
+ burstIndex: i,
44
+ cycleCount: 0,
45
+ nextTime: b.time,
46
+ })),
47
+ };
48
+ }
49
+ createDeadParticle() {
50
+ return {
51
+ position: new Float32Array([0, 0, 0]),
52
+ velocity: new Float32Array([0, 0, 0]),
53
+ startColor: new Float32Array([1, 1, 1, 1]),
54
+ color: new Float32Array([1, 1, 1, 1]),
55
+ startSize: new Float32Array([1, 1, 1]),
56
+ size: new Float32Array([1, 1, 1]),
57
+ rotation: new Float32Array([0, 0, 0]),
58
+ angularVelocity: new Float32Array([0, 0, 0]),
59
+ startLifetime: 0,
60
+ remainingLifetime: -1,
61
+ normalizedLifetime: 1,
62
+ randomSeed: (Math.random() * 0xffffffff) | 0,
63
+ startSpeed: 0,
64
+ axisOfRotation: new Float32Array([0, 0, 1]),
65
+ meshIndex: 0,
66
+ customData1: new Float32Array([0, 0, 0, 0]),
67
+ customData2: new Float32Array([0, 0, 0, 0]),
68
+ };
69
+ }
70
+ // ─── Playback Control ──────────────────────────────────
71
+ play() {
72
+ if (this.state.isPaused) {
73
+ this.state.isPaused = false;
74
+ }
75
+ this.state.isPlaying = true;
76
+ this.state.isStopped = false;
77
+ this.state.isEmitting = true;
78
+ if (this.definition.main.prewarm) {
79
+ this.prewarm();
80
+ }
81
+ }
82
+ pause() {
83
+ this.state.isPaused = true;
84
+ }
85
+ stop(clear = true) {
86
+ this.state.isPlaying = false;
87
+ this.state.isStopped = true;
88
+ this.state.isEmitting = false;
89
+ if (clear) {
90
+ this.clear();
91
+ }
92
+ }
93
+ clear() {
94
+ this.state.activeCount = 0;
95
+ this.state.time = 0;
96
+ this.state.emitCounter = 0;
97
+ this.state.distanceTraveled = 0;
98
+ for (const particle of this.state.particles) {
99
+ particle.remainingLifetime = -1;
100
+ }
101
+ this.resetBurstStates();
102
+ }
103
+ resetBurstStates() {
104
+ this.state.burstStates = this.definition.emission.bursts.map((b, i) => ({
105
+ burstIndex: i,
106
+ cycleCount: 0,
107
+ nextTime: b.time,
108
+ }));
109
+ }
110
+ prewarm() {
111
+ const duration = this.definition.main.duration;
112
+ const dt = 1 / 60;
113
+ const steps = Math.ceil(duration / dt);
114
+ for (let i = 0; i < steps; i++) {
115
+ this.simulate(dt);
116
+ }
117
+ }
118
+ // ─── Emission ──────────────────────────────────────────
119
+ emit(count, position) {
120
+ for (let i = 0; i < count; i++) {
121
+ this.emitSingleParticle(position);
122
+ }
123
+ }
124
+ emitSingleParticle(overridePosition) {
125
+ // Find dead particle
126
+ const deadIndex = this.findDeadParticleIndex();
127
+ if (deadIndex === -1)
128
+ return false;
129
+ const particle = this.state.particles[deadIndex];
130
+ this.initializeParticle(particle, overridePosition);
131
+ this.state.activeCount++;
132
+ return true;
133
+ }
134
+ findDeadParticleIndex() {
135
+ for (let i = 0; i < this.state.particles.length; i++) {
136
+ if (this.state.particles[i].remainingLifetime < 0) {
137
+ return i;
138
+ }
139
+ }
140
+ return -1;
141
+ }
142
+ initializeParticle(particle, overridePosition) {
143
+ const main = this.definition.main;
144
+ // Random seed for this particle
145
+ particle.randomSeed = (Math.random() * 0xffffffff) | 0;
146
+ // Position and velocity from shape
147
+ const { position, velocity } = this.sampleEmitterShape();
148
+ if (overridePosition) {
149
+ particle.position.set(overridePosition);
150
+ }
151
+ else {
152
+ particle.position.set(position);
153
+ }
154
+ // Initial velocity
155
+ const startSpeed = this.evaluateMinMaxCurve(main.startSpeed, 0, particle.randomSeed);
156
+ particle.startSpeed = startSpeed;
157
+ particle.velocity[0] = velocity[0] * startSpeed;
158
+ particle.velocity[1] = velocity[1] * startSpeed;
159
+ particle.velocity[2] = velocity[2] * startSpeed;
160
+ // Lifetime
161
+ particle.startLifetime = this.evaluateMinMaxCurve(main.startLifetime, 0, particle.randomSeed);
162
+ particle.remainingLifetime = particle.startLifetime;
163
+ particle.normalizedLifetime = 0;
164
+ // Size
165
+ if (main.startSize3D) {
166
+ particle.startSize[0] = this.evaluateMinMaxCurve(main.startSizeX, 0, particle.randomSeed);
167
+ particle.startSize[1] = this.evaluateMinMaxCurve(main.startSizeY, 0, particle.randomSeed);
168
+ particle.startSize[2] = this.evaluateMinMaxCurve(main.startSizeZ, 0, particle.randomSeed);
169
+ }
170
+ else {
171
+ const size = this.evaluateMinMaxCurve(main.startSize, 0, particle.randomSeed);
172
+ particle.startSize.set([size, size, size]);
173
+ }
174
+ particle.size.set(particle.startSize);
175
+ // Rotation
176
+ if (main.startRotation3D) {
177
+ particle.rotation[0] = this.evaluateMinMaxCurve(main.startRotationX, 0, particle.randomSeed);
178
+ particle.rotation[1] = this.evaluateMinMaxCurve(main.startRotationY, 0, particle.randomSeed);
179
+ particle.rotation[2] = this.evaluateMinMaxCurve(main.startRotationZ, 0, particle.randomSeed);
180
+ }
181
+ else {
182
+ particle.rotation[2] = this.evaluateMinMaxCurve(main.startRotation, 0, particle.randomSeed);
183
+ }
184
+ // Flip rotation
185
+ if (main.flipRotation > 0 && Math.random() < main.flipRotation) {
186
+ particle.rotation[2] += Math.PI;
187
+ }
188
+ // Color
189
+ const color = this.evaluateMinMaxGradient(main.startColor, 0, particle.randomSeed);
190
+ particle.startColor.set(color);
191
+ particle.color.set(color);
192
+ // Angular velocity (will be set by rotationOverLifetime)
193
+ particle.angularVelocity.set([0, 0, 0]);
194
+ }
195
+ sampleEmitterShape() {
196
+ const shape = this.definition.shape;
197
+ switch (shape.type) {
198
+ case 'sphere':
199
+ return this.sampleSphere(shape);
200
+ case 'hemisphere':
201
+ return this.sampleHemisphere(shape);
202
+ case 'cone':
203
+ return this.sampleCone(shape);
204
+ case 'box':
205
+ return this.sampleBox(shape);
206
+ case 'circle':
207
+ return this.sampleCircle(shape);
208
+ default:
209
+ return { position: [0, 0, 0], velocity: [0, 1, 0] };
210
+ }
211
+ }
212
+ sampleSphere(shape) {
213
+ // Random direction
214
+ const theta = Math.random() * 2 * Math.PI;
215
+ const phi = Math.acos(2 * Math.random() - 1);
216
+ const sinPhi = Math.sin(phi);
217
+ const dir = [sinPhi * Math.cos(theta), sinPhi * Math.sin(theta), Math.cos(phi)];
218
+ // Position on/in sphere
219
+ const radiusFactor = shape.radiusThickness === 1
220
+ ? Math.cbrt(Math.random())
221
+ : 1 - Math.random() * shape.radiusThickness;
222
+ const r = shape.radius * radiusFactor;
223
+ const position = [
224
+ dir[0] * r + shape.position[0],
225
+ dir[1] * r + shape.position[1],
226
+ dir[2] * r + shape.position[2],
227
+ ];
228
+ return { position, velocity: dir };
229
+ }
230
+ sampleHemisphere(shape) {
231
+ const theta = Math.random() * 2 * Math.PI;
232
+ const phi = Math.acos(Math.random()); // 0 to PI/2
233
+ const sinPhi = Math.sin(phi);
234
+ const dir = [sinPhi * Math.cos(theta), Math.cos(phi), sinPhi * Math.sin(theta)];
235
+ const radiusFactor = shape.radiusThickness === 1
236
+ ? Math.cbrt(Math.random())
237
+ : 1 - Math.random() * shape.radiusThickness;
238
+ const r = shape.radius * radiusFactor;
239
+ const position = [
240
+ dir[0] * r + shape.position[0],
241
+ dir[1] * r + shape.position[1],
242
+ dir[2] * r + shape.position[2],
243
+ ];
244
+ return { position, velocity: dir };
245
+ }
246
+ sampleCone(shape) {
247
+ const angleRad = (shape.angle * Math.PI) / 180;
248
+ const theta = Math.random() * 2 * Math.PI;
249
+ // Random angle within cone
250
+ const coneAngle = Math.random() * angleRad;
251
+ const sinAngle = Math.sin(coneAngle);
252
+ const cosAngle = Math.cos(coneAngle);
253
+ const dir = [sinAngle * Math.cos(theta), cosAngle, sinAngle * Math.sin(theta)];
254
+ // Position based on emitFrom
255
+ let position;
256
+ switch (shape.emitFrom) {
257
+ case 'base':
258
+ const r = shape.radius * Math.sqrt(Math.random());
259
+ position = [
260
+ r * Math.cos(theta) + shape.position[0],
261
+ shape.position[1],
262
+ r * Math.sin(theta) + shape.position[2],
263
+ ];
264
+ break;
265
+ case 'volume':
266
+ const height = Math.random() * shape.length;
267
+ const radiusAtHeight = shape.radius + height * Math.tan(angleRad);
268
+ const rV = radiusAtHeight * Math.sqrt(Math.random());
269
+ position = [
270
+ rV * Math.cos(theta) + shape.position[0],
271
+ height + shape.position[1],
272
+ rV * Math.sin(theta) + shape.position[2],
273
+ ];
274
+ break;
275
+ default:
276
+ position = [...shape.position];
277
+ }
278
+ return { position, velocity: dir };
279
+ }
280
+ sampleBox(shape) {
281
+ const hw = shape.scale[0] / 2;
282
+ const hh = shape.scale[1] / 2;
283
+ const hd = shape.scale[2] / 2;
284
+ let position;
285
+ switch (shape.emitFrom) {
286
+ case 'volume':
287
+ position = [
288
+ (Math.random() * 2 - 1) * hw + shape.position[0],
289
+ (Math.random() * 2 - 1) * hh + shape.position[1],
290
+ (Math.random() * 2 - 1) * hd + shape.position[2],
291
+ ];
292
+ break;
293
+ case 'shell':
294
+ // Random face
295
+ const face = Math.floor(Math.random() * 6);
296
+ const u = Math.random() * 2 - 1;
297
+ const v = Math.random() * 2 - 1;
298
+ switch (face) {
299
+ case 0:
300
+ position = [hw, u * hh, v * hd];
301
+ break;
302
+ case 1:
303
+ position = [-hw, u * hh, v * hd];
304
+ break;
305
+ case 2:
306
+ position = [u * hw, hh, v * hd];
307
+ break;
308
+ case 3:
309
+ position = [u * hw, -hh, v * hd];
310
+ break;
311
+ case 4:
312
+ position = [u * hw, v * hh, hd];
313
+ break;
314
+ default:
315
+ position = [u * hw, v * hh, -hd];
316
+ break;
317
+ }
318
+ position[0] += shape.position[0];
319
+ position[1] += shape.position[1];
320
+ position[2] += shape.position[2];
321
+ break;
322
+ default:
323
+ position = [...shape.position];
324
+ }
325
+ return { position, velocity: [0, 1, 0] };
326
+ }
327
+ sampleCircle(shape) {
328
+ const theta = Math.random() * 2 * Math.PI;
329
+ const radiusFactor = shape.radiusThickness === 1
330
+ ? Math.sqrt(Math.random())
331
+ : 1 - Math.random() * shape.radiusThickness;
332
+ const r = shape.radius * radiusFactor;
333
+ const position = [
334
+ r * Math.cos(theta) + shape.position[0],
335
+ shape.position[1],
336
+ r * Math.sin(theta) + shape.position[2],
337
+ ];
338
+ return { position, velocity: [0, 1, 0] };
339
+ }
340
+ // ─── Simulation ────────────────────────────────────────
341
+ simulate(deltaTime) {
342
+ if (!this.state.isPlaying || this.state.isPaused)
343
+ return;
344
+ const dt = deltaTime * this.definition.main.simulationSpeed;
345
+ // Update time
346
+ this.state.time += dt;
347
+ // Handle looping
348
+ if (this.definition.main.looping && this.state.time >= this.definition.main.duration) {
349
+ this.state.time = this.state.time % this.definition.main.duration;
350
+ this.resetBurstStates();
351
+ }
352
+ // Emit particles
353
+ if (this.state.isEmitting) {
354
+ this.processEmission(dt);
355
+ }
356
+ // Update particles
357
+ for (let i = 0; i < this.state.particles.length; i++) {
358
+ const particle = this.state.particles[i];
359
+ if (particle.remainingLifetime >= 0) {
360
+ this.updateParticle(particle, dt);
361
+ }
362
+ }
363
+ }
364
+ processEmission(dt) {
365
+ const emission = this.definition.emission;
366
+ if (!emission.enabled)
367
+ return;
368
+ // Rate over time
369
+ const rateOverTime = this.evaluateMinMaxCurve(emission.rateOverTime, this.normalizedTime(), 0);
370
+ this.state.emitCounter += rateOverTime * dt;
371
+ while (this.state.emitCounter >= 1) {
372
+ this.emitSingleParticle();
373
+ this.state.emitCounter -= 1;
374
+ }
375
+ // Bursts
376
+ for (const burstState of this.state.burstStates) {
377
+ const burst = emission.bursts[burstState.burstIndex];
378
+ while (this.state.time >= burstState.nextTime && burstState.cycleCount < burst.cycles) {
379
+ // Probability check
380
+ if (Math.random() < burst.probability) {
381
+ const count = Math.round(this.evaluateMinMaxCurve(burst.count, 0, (Math.random() * 0xffffffff) | 0));
382
+ this.emit(count);
383
+ }
384
+ burstState.cycleCount++;
385
+ burstState.nextTime += burst.interval;
386
+ }
387
+ }
388
+ }
389
+ updateParticle(particle, dt) {
390
+ // Update lifetime
391
+ particle.remainingLifetime -= dt;
392
+ if (particle.remainingLifetime < 0) {
393
+ // Particle died
394
+ this.state.activeCount--;
395
+ // Trigger sub-emitter death
396
+ this.triggerSubEmitters('death', particle);
397
+ return;
398
+ }
399
+ particle.normalizedLifetime = 1 - particle.remainingLifetime / particle.startLifetime;
400
+ // Apply forces
401
+ this.applyForces(particle, dt);
402
+ // Apply velocity/force over lifetime modules
403
+ this.applyVelocityOverLifetime(particle, dt);
404
+ this.applyForceOverLifetime(particle, dt);
405
+ this.applyLimitVelocityOverLifetime(particle, dt);
406
+ // Integrate position
407
+ particle.position[0] += particle.velocity[0] * dt;
408
+ particle.position[1] += particle.velocity[1] * dt;
409
+ particle.position[2] += particle.velocity[2] * dt;
410
+ // Apply noise
411
+ this.applyNoise(particle, dt);
412
+ // Update visual properties
413
+ this.applyColorOverLifetime(particle);
414
+ this.applySizeOverLifetime(particle);
415
+ this.applyRotationOverLifetime(particle, dt);
416
+ // Speed-based modifications
417
+ this.applyColorBySpeed(particle);
418
+ this.applySizeBySpeed(particle);
419
+ this.applyRotationBySpeed(particle, dt);
420
+ // Handle collision
421
+ if (this.definition.collision.enabled) {
422
+ this.handleCollision(particle);
423
+ }
424
+ }
425
+ applyForces(particle, dt) {
426
+ const main = this.definition.main;
427
+ // Gravity
428
+ const gravity = this.evaluateMinMaxCurve(main.gravityModifier, particle.normalizedLifetime, particle.randomSeed);
429
+ particle.velocity[1] -= 9.81 * gravity * dt;
430
+ // External force fields
431
+ if (this.definition.externalForces.enabled) {
432
+ for (const field of this.definition.externalForces.forceFields) {
433
+ this.applyForceField(particle, field, dt);
434
+ }
435
+ }
436
+ }
437
+ applyForceField(particle, field, dt) {
438
+ switch (field.type) {
439
+ case 'gravity':
440
+ const strength = field.strength;
441
+ particle.velocity[0] += field.direction[0] * strength * dt;
442
+ particle.velocity[1] += field.direction[1] * strength * dt;
443
+ particle.velocity[2] += field.direction[2] * strength * dt;
444
+ break;
445
+ case 'directional':
446
+ const dirStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
447
+ particle.velocity[0] += field.direction[0] * dirStrength * dt;
448
+ particle.velocity[1] += field.direction[1] * dirStrength * dt;
449
+ particle.velocity[2] += field.direction[2] * dirStrength * dt;
450
+ break;
451
+ case 'vortex':
452
+ const vortexStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
453
+ const toCenter = [
454
+ field.center[0] - particle.position[0],
455
+ field.center[1] - particle.position[1],
456
+ field.center[2] - particle.position[2],
457
+ ];
458
+ const dist = Math.sqrt(toCenter[0] ** 2 + toCenter[1] ** 2 + toCenter[2] ** 2);
459
+ if (dist > 0.001 && dist < field.radius) {
460
+ // Tangential force (cross product with axis)
461
+ const tangent = [
462
+ field.axis[1] * toCenter[2] - field.axis[2] * toCenter[1],
463
+ field.axis[2] * toCenter[0] - field.axis[0] * toCenter[2],
464
+ field.axis[0] * toCenter[1] - field.axis[1] * toCenter[0],
465
+ ];
466
+ const tangentLen = Math.sqrt(tangent[0] ** 2 + tangent[1] ** 2 + tangent[2] ** 2);
467
+ if (tangentLen > 0.001) {
468
+ const factor = (vortexStrength * (1 - dist / field.radius)) / tangentLen;
469
+ particle.velocity[0] += tangent[0] * factor * dt;
470
+ particle.velocity[1] += tangent[1] * factor * dt;
471
+ particle.velocity[2] += tangent[2] * factor * dt;
472
+ }
473
+ // Attraction
474
+ if (field.attractionStrength > 0) {
475
+ const attractFactor = (field.attractionStrength * (1 - dist / field.radius)) / dist;
476
+ particle.velocity[0] += toCenter[0] * attractFactor * dt;
477
+ particle.velocity[1] += toCenter[1] * attractFactor * dt;
478
+ particle.velocity[2] += toCenter[2] * attractFactor * dt;
479
+ }
480
+ }
481
+ break;
482
+ case 'drag':
483
+ const dragCoeff = this.evaluateMinMaxCurve(field.coefficient, particle.normalizedLifetime, particle.randomSeed);
484
+ let drag = dragCoeff;
485
+ if (field.multiplyBySize) {
486
+ const sizeMag = Math.sqrt(particle.size[0] ** 2 + particle.size[1] ** 2 + particle.size[2] ** 2);
487
+ drag *= sizeMag;
488
+ }
489
+ if (field.multiplyByVelocity) {
490
+ const velMag = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
491
+ drag *= velMag;
492
+ }
493
+ const factor = Math.max(0, 1 - drag * dt);
494
+ particle.velocity[0] *= factor;
495
+ particle.velocity[1] *= factor;
496
+ particle.velocity[2] *= factor;
497
+ break;
498
+ case 'attractor':
499
+ const attractorStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
500
+ const toAttractor = [
501
+ field.position[0] - particle.position[0],
502
+ field.position[1] - particle.position[1],
503
+ field.position[2] - particle.position[2],
504
+ ];
505
+ const attractorDist = Math.sqrt(toAttractor[0] ** 2 + toAttractor[1] ** 2 + toAttractor[2] ** 2);
506
+ if (attractorDist > 0.001 && attractorDist < field.radius) {
507
+ const falloff = 1 - attractorDist / field.radius;
508
+ const force = (attractorStrength * falloff) / attractorDist;
509
+ particle.velocity[0] += toAttractor[0] * force * dt;
510
+ particle.velocity[1] += toAttractor[1] * force * dt;
511
+ particle.velocity[2] += toAttractor[2] * force * dt;
512
+ if (field.killOnArrive && attractorDist < 0.1) {
513
+ particle.remainingLifetime = -1;
514
+ }
515
+ }
516
+ break;
517
+ }
518
+ }
519
+ applyVelocityOverLifetime(particle, dt) {
520
+ const vol = this.definition.velocityOverLifetime;
521
+ if (!vol.enabled)
522
+ return;
523
+ // Linear velocity
524
+ if (vol.linear.mode !== 'constant' ||
525
+ vol.linear.value[0] !== 0 ||
526
+ vol.linear.value[1] !== 0 ||
527
+ vol.linear.value[2] !== 0) {
528
+ const linear = this.evaluateMinMaxVector3(vol.linear, particle.normalizedLifetime, particle.randomSeed);
529
+ particle.velocity[0] += linear[0] * dt;
530
+ particle.velocity[1] += linear[1] * dt;
531
+ particle.velocity[2] += linear[2] * dt;
532
+ }
533
+ // Radial velocity
534
+ const radial = this.evaluateMinMaxCurve(vol.radial, particle.normalizedLifetime, particle.randomSeed);
535
+ if (radial !== 0) {
536
+ const len = Math.sqrt(particle.position[0] ** 2 + particle.position[1] ** 2 + particle.position[2] ** 2);
537
+ if (len > 0.001) {
538
+ particle.velocity[0] += (particle.position[0] / len) * radial * dt;
539
+ particle.velocity[1] += (particle.position[1] / len) * radial * dt;
540
+ particle.velocity[2] += (particle.position[2] / len) * radial * dt;
541
+ }
542
+ }
543
+ // Speed modifier
544
+ const speedMod = this.evaluateMinMaxCurve(vol.speedModifier, particle.normalizedLifetime, particle.randomSeed);
545
+ particle.velocity[0] *= speedMod;
546
+ particle.velocity[1] *= speedMod;
547
+ particle.velocity[2] *= speedMod;
548
+ }
549
+ applyForceOverLifetime(particle, dt) {
550
+ const fol = this.definition.forceOverLifetime;
551
+ if (!fol.enabled)
552
+ return;
553
+ const seed = fol.randomize ? (Math.random() * 0xffffffff) | 0 : particle.randomSeed;
554
+ particle.velocity[0] += this.evaluateMinMaxCurve(fol.x, particle.normalizedLifetime, seed) * dt;
555
+ particle.velocity[1] += this.evaluateMinMaxCurve(fol.y, particle.normalizedLifetime, seed) * dt;
556
+ particle.velocity[2] += this.evaluateMinMaxCurve(fol.z, particle.normalizedLifetime, seed) * dt;
557
+ }
558
+ applyLimitVelocityOverLifetime(particle, dt) {
559
+ const lvol = this.definition.limitVelocityOverLifetime;
560
+ if (!lvol.enabled)
561
+ return;
562
+ const velMag = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
563
+ let maxSpeed;
564
+ if (lvol.separateAxes) {
565
+ // Per-axis limit (simplified)
566
+ maxSpeed = this.evaluateMinMaxCurve(lvol.speed, particle.normalizedLifetime, particle.randomSeed);
567
+ }
568
+ else {
569
+ maxSpeed = this.evaluateMinMaxCurve(lvol.speed, particle.normalizedLifetime, particle.randomSeed);
570
+ }
571
+ if (velMag > maxSpeed) {
572
+ const dampenFactor = 1 - lvol.dampen;
573
+ const targetSpeed = maxSpeed + (velMag - maxSpeed) * dampenFactor;
574
+ const scale = targetSpeed / velMag;
575
+ particle.velocity[0] *= scale;
576
+ particle.velocity[1] *= scale;
577
+ particle.velocity[2] *= scale;
578
+ }
579
+ // Drag
580
+ const drag = this.evaluateMinMaxCurve(lvol.drag, particle.normalizedLifetime, particle.randomSeed);
581
+ if (drag > 0) {
582
+ const dragFactor = Math.max(0, 1 - drag * dt);
583
+ particle.velocity[0] *= dragFactor;
584
+ particle.velocity[1] *= dragFactor;
585
+ particle.velocity[2] *= dragFactor;
586
+ }
587
+ }
588
+ applyNoise(particle, dt) {
589
+ const noise = this.definition.noise;
590
+ if (!noise.enabled)
591
+ return;
592
+ const strength = this.evaluateMinMaxCurve(noise.strength, particle.normalizedLifetime, particle.randomSeed);
593
+ const freq = noise.frequency;
594
+ const scroll = noise.scrollSpeed * this.state.time;
595
+ // Simple noise approximation
596
+ const nx = particle.position[0] * freq + scroll;
597
+ const ny = particle.position[1] * freq + scroll;
598
+ const nz = particle.position[2] * freq + scroll;
599
+ // Pseudo-noise
600
+ const noiseX = (Math.sin(nx * 12.9898 + ny * 78.233) * 43758.5453) % 1;
601
+ const noiseY = (Math.sin(ny * 12.9898 + nz * 78.233) * 43758.5453) % 1;
602
+ const noiseZ = (Math.sin(nz * 12.9898 + nx * 78.233) * 43758.5453) % 1;
603
+ const posAmount = this.evaluateMinMaxCurve(noise.positionAmount, particle.normalizedLifetime, particle.randomSeed);
604
+ particle.position[0] += (noiseX * 2 - 1) * strength * posAmount * dt;
605
+ particle.position[1] += (noiseY * 2 - 1) * strength * posAmount * dt;
606
+ particle.position[2] += (noiseZ * 2 - 1) * strength * posAmount * dt;
607
+ }
608
+ applyColorOverLifetime(particle) {
609
+ const col = this.definition.colorOverLifetime;
610
+ if (!col.enabled)
611
+ return;
612
+ const color = this.evaluateMinMaxGradient(col.color, particle.normalizedLifetime, particle.randomSeed);
613
+ // Multiply with start color
614
+ particle.color[0] = particle.startColor[0] * color[0];
615
+ particle.color[1] = particle.startColor[1] * color[1];
616
+ particle.color[2] = particle.startColor[2] * color[2];
617
+ particle.color[3] = particle.startColor[3] * color[3];
618
+ }
619
+ applySizeOverLifetime(particle) {
620
+ const sol = this.definition.sizeOverLifetime;
621
+ if (!sol.enabled)
622
+ return;
623
+ if (sol.separateAxes) {
624
+ particle.size[0] =
625
+ particle.startSize[0] *
626
+ this.evaluateMinMaxCurve(sol.sizeX, particle.normalizedLifetime, particle.randomSeed);
627
+ particle.size[1] =
628
+ particle.startSize[1] *
629
+ this.evaluateMinMaxCurve(sol.sizeY, particle.normalizedLifetime, particle.randomSeed);
630
+ particle.size[2] =
631
+ particle.startSize[2] *
632
+ this.evaluateMinMaxCurve(sol.sizeZ, particle.normalizedLifetime, particle.randomSeed);
633
+ }
634
+ else {
635
+ const scale = this.evaluateMinMaxCurve(sol.size, particle.normalizedLifetime, particle.randomSeed);
636
+ particle.size[0] = particle.startSize[0] * scale;
637
+ particle.size[1] = particle.startSize[1] * scale;
638
+ particle.size[2] = particle.startSize[2] * scale;
639
+ }
640
+ }
641
+ applyRotationOverLifetime(particle, dt) {
642
+ const rol = this.definition.rotationOverLifetime;
643
+ if (!rol.enabled)
644
+ return;
645
+ if (rol.separateAxes) {
646
+ particle.rotation[0] +=
647
+ this.evaluateMinMaxCurve(rol.angularVelocityX, particle.normalizedLifetime, particle.randomSeed) * dt;
648
+ particle.rotation[1] +=
649
+ this.evaluateMinMaxCurve(rol.angularVelocityY, particle.normalizedLifetime, particle.randomSeed) * dt;
650
+ particle.rotation[2] +=
651
+ this.evaluateMinMaxCurve(rol.angularVelocityZ, particle.normalizedLifetime, particle.randomSeed) * dt;
652
+ }
653
+ else {
654
+ particle.rotation[2] +=
655
+ this.evaluateMinMaxCurve(rol.angularVelocity, particle.normalizedLifetime, particle.randomSeed) * dt;
656
+ }
657
+ }
658
+ applyColorBySpeed(particle) {
659
+ const cbs = this.definition.colorBySpeed;
660
+ if (!cbs.enabled)
661
+ return;
662
+ const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
663
+ const t = Math.min(1, Math.max(0, (speed - cbs.speedRange[0]) / (cbs.speedRange[1] - cbs.speedRange[0])));
664
+ const color = this.evaluateMinMaxGradient(cbs.color, t, particle.randomSeed);
665
+ particle.color[0] *= color[0];
666
+ particle.color[1] *= color[1];
667
+ particle.color[2] *= color[2];
668
+ particle.color[3] *= color[3];
669
+ }
670
+ applySizeBySpeed(particle) {
671
+ const sbs = this.definition.sizeBySpeed;
672
+ if (!sbs.enabled)
673
+ return;
674
+ const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
675
+ const t = Math.min(1, Math.max(0, (speed - sbs.speedRange[0]) / (sbs.speedRange[1] - sbs.speedRange[0])));
676
+ if (sbs.separateAxes) {
677
+ particle.size[0] *= this.evaluateMinMaxCurve(sbs.sizeX, t, particle.randomSeed);
678
+ particle.size[1] *= this.evaluateMinMaxCurve(sbs.sizeY, t, particle.randomSeed);
679
+ particle.size[2] *= this.evaluateMinMaxCurve(sbs.sizeZ, t, particle.randomSeed);
680
+ }
681
+ else {
682
+ const scale = this.evaluateMinMaxCurve(sbs.size, t, particle.randomSeed);
683
+ particle.size[0] *= scale;
684
+ particle.size[1] *= scale;
685
+ particle.size[2] *= scale;
686
+ }
687
+ }
688
+ applyRotationBySpeed(particle, dt) {
689
+ const rbs = this.definition.rotationBySpeed;
690
+ if (!rbs.enabled)
691
+ return;
692
+ const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
693
+ const t = Math.min(1, Math.max(0, (speed - rbs.speedRange[0]) / (rbs.speedRange[1] - rbs.speedRange[0])));
694
+ if (rbs.separateAxes) {
695
+ particle.rotation[0] +=
696
+ this.evaluateMinMaxCurve(rbs.angularVelocityX, t, particle.randomSeed) * dt;
697
+ particle.rotation[1] +=
698
+ this.evaluateMinMaxCurve(rbs.angularVelocityY, t, particle.randomSeed) * dt;
699
+ particle.rotation[2] +=
700
+ this.evaluateMinMaxCurve(rbs.angularVelocityZ, t, particle.randomSeed) * dt;
701
+ }
702
+ else {
703
+ particle.rotation[2] +=
704
+ this.evaluateMinMaxCurve(rbs.angularVelocity, t, particle.randomSeed) * dt;
705
+ }
706
+ }
707
+ handleCollision(particle) {
708
+ const collision = this.definition.collision;
709
+ if (collision.type === 'planes') {
710
+ for (const plane of collision.planes) {
711
+ const dot = particle.position[0] * plane.normal[0] +
712
+ particle.position[1] * plane.normal[1] +
713
+ particle.position[2] * plane.normal[2];
714
+ const planeDist = plane.position[0] * plane.normal[0] +
715
+ plane.position[1] * plane.normal[1] +
716
+ plane.position[2] * plane.normal[2];
717
+ if (dot < planeDist) {
718
+ // Below plane - collision occurred
719
+ const velDotN = particle.velocity[0] * plane.normal[0] +
720
+ particle.velocity[1] * plane.normal[1] +
721
+ particle.velocity[2] * plane.normal[2];
722
+ const bounce = this.evaluateMinMaxCurve(collision.bounce, particle.normalizedLifetime, particle.randomSeed);
723
+ const dampen = this.evaluateMinMaxCurve(collision.dampen, particle.normalizedLifetime, particle.randomSeed);
724
+ // Reflect velocity
725
+ particle.velocity[0] -= (1 + bounce) * velDotN * plane.normal[0];
726
+ particle.velocity[1] -= (1 + bounce) * velDotN * plane.normal[1];
727
+ particle.velocity[2] -= (1 + bounce) * velDotN * plane.normal[2];
728
+ // Apply damping
729
+ particle.velocity[0] *= 1 - dampen;
730
+ particle.velocity[1] *= 1 - dampen;
731
+ particle.velocity[2] *= 1 - dampen;
732
+ // Move back to plane
733
+ particle.position[0] += (planeDist - dot) * plane.normal[0];
734
+ particle.position[1] += (planeDist - dot) * plane.normal[1];
735
+ particle.position[2] += (planeDist - dot) * plane.normal[2];
736
+ // Lifetime loss
737
+ const lifetimeLoss = this.evaluateMinMaxCurve(collision.lifetimeLoss, particle.normalizedLifetime, particle.randomSeed);
738
+ particle.remainingLifetime -= particle.startLifetime * lifetimeLoss;
739
+ // Kill if too slow
740
+ const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
741
+ if (speed < collision.minKillSpeed) {
742
+ particle.remainingLifetime = -1;
743
+ }
744
+ // Trigger sub-emitter
745
+ this.triggerSubEmitters('collision', particle);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ triggerSubEmitters(trigger, particle) {
751
+ for (const subEmitter of this.definition.subEmitters) {
752
+ if (subEmitter.trigger === trigger && Math.random() < subEmitter.probability) ;
753
+ }
754
+ }
755
+ // ─── Curve/Gradient Evaluation ─────────────────────────
756
+ evaluateMinMaxCurve(curve, t, seed) {
757
+ switch (curve.mode) {
758
+ case 'constant':
759
+ return curve.value;
760
+ case 'curve':
761
+ return this.evaluateAnimationCurve(curve.curve, t) * curve.multiplier;
762
+ case 'randomBetweenTwoConstants':
763
+ const rand = this.seededRandom(seed);
764
+ return curve.min + (curve.max - curve.min) * rand;
765
+ case 'randomBetweenTwoCurves':
766
+ const randCurve = this.seededRandom(seed);
767
+ const minVal = this.evaluateAnimationCurve(curve.curveMin, t);
768
+ const maxVal = this.evaluateAnimationCurve(curve.curveMax, t);
769
+ return (minVal + (maxVal - minVal) * randCurve) * curve.multiplier;
770
+ }
771
+ }
772
+ evaluateAnimationCurve(curve, t) {
773
+ const keyframes = curve.keyframes;
774
+ if (keyframes.length === 0)
775
+ return 0;
776
+ if (keyframes.length === 1)
777
+ return keyframes[0].value;
778
+ // Handle wrap modes
779
+ if (t < 0)
780
+ t = this.handleWrapMode(t, curve.preWrapMode);
781
+ if (t > 1)
782
+ t = this.handleWrapMode(t, curve.postWrapMode);
783
+ // Find keyframes
784
+ for (let i = 0; i < keyframes.length - 1; i++) {
785
+ const k0 = keyframes[i];
786
+ const k1 = keyframes[i + 1];
787
+ if (t >= k0.time && t <= k1.time) {
788
+ const localT = (t - k0.time) / (k1.time - k0.time);
789
+ // Hermite interpolation
790
+ const t2 = localT * localT;
791
+ const t3 = t2 * localT;
792
+ const h00 = 2 * t3 - 3 * t2 + 1;
793
+ const h10 = t3 - 2 * t2 + localT;
794
+ const h01 = -2 * t3 + 3 * t2;
795
+ const h11 = t3 - t2;
796
+ const dt = k1.time - k0.time;
797
+ return h00 * k0.value + h10 * dt * k0.outTangent + h01 * k1.value + h11 * dt * k1.inTangent;
798
+ }
799
+ }
800
+ return keyframes[keyframes.length - 1].value;
801
+ }
802
+ handleWrapMode(t, mode) {
803
+ switch (mode) {
804
+ case 'clamp':
805
+ return Math.max(0, Math.min(1, t));
806
+ case 'loop':
807
+ return t - Math.floor(t);
808
+ case 'pingPong':
809
+ const cycle = Math.floor(t);
810
+ const frac = t - cycle;
811
+ return cycle % 2 === 0 ? frac : 1 - frac;
812
+ }
813
+ }
814
+ evaluateMinMaxGradient(gradient, t, seed) {
815
+ switch (gradient.mode) {
816
+ case 'color':
817
+ return gradient.color;
818
+ case 'gradient':
819
+ return this.evaluateGradient(gradient.gradient, t);
820
+ case 'randomBetweenTwoColors':
821
+ const rand = this.seededRandom(seed);
822
+ return [
823
+ gradient.colorMin[0] + (gradient.colorMax[0] - gradient.colorMin[0]) * rand,
824
+ gradient.colorMin[1] + (gradient.colorMax[1] - gradient.colorMin[1]) * rand,
825
+ gradient.colorMin[2] + (gradient.colorMax[2] - gradient.colorMin[2]) * rand,
826
+ gradient.colorMin[3] + (gradient.colorMax[3] - gradient.colorMin[3]) * rand,
827
+ ];
828
+ case 'randomBetweenTwoGradients':
829
+ const randG = this.seededRandom(seed);
830
+ const minColor = this.evaluateGradient(gradient.gradientMin, t);
831
+ const maxColor = this.evaluateGradient(gradient.gradientMax, t);
832
+ return [
833
+ minColor[0] + (maxColor[0] - minColor[0]) * randG,
834
+ minColor[1] + (maxColor[1] - minColor[1]) * randG,
835
+ minColor[2] + (maxColor[2] - minColor[2]) * randG,
836
+ minColor[3] + (maxColor[3] - minColor[3]) * randG,
837
+ ];
838
+ }
839
+ }
840
+ evaluateGradient(gradient, t) {
841
+ // Evaluate color
842
+ let color = [1, 1, 1];
843
+ const colorKeys = gradient.colorKeys;
844
+ if (colorKeys.length > 0) {
845
+ if (t <= colorKeys[0].time) {
846
+ color = colorKeys[0].color;
847
+ }
848
+ else if (t >= colorKeys[colorKeys.length - 1].time) {
849
+ color = colorKeys[colorKeys.length - 1].color;
850
+ }
851
+ else {
852
+ for (let i = 0; i < colorKeys.length - 1; i++) {
853
+ if (t >= colorKeys[i].time && t <= colorKeys[i + 1].time) {
854
+ const localT = (t - colorKeys[i].time) / (colorKeys[i + 1].time - colorKeys[i].time);
855
+ color = [
856
+ colorKeys[i].color[0] + (colorKeys[i + 1].color[0] - colorKeys[i].color[0]) * localT,
857
+ colorKeys[i].color[1] + (colorKeys[i + 1].color[1] - colorKeys[i].color[1]) * localT,
858
+ colorKeys[i].color[2] + (colorKeys[i + 1].color[2] - colorKeys[i].color[2]) * localT,
859
+ ];
860
+ break;
861
+ }
862
+ }
863
+ }
864
+ }
865
+ // Evaluate alpha
866
+ let alpha = 1;
867
+ const alphaKeys = gradient.alphaKeys;
868
+ if (alphaKeys.length > 0) {
869
+ if (t <= alphaKeys[0].time) {
870
+ alpha = alphaKeys[0].alpha;
871
+ }
872
+ else if (t >= alphaKeys[alphaKeys.length - 1].time) {
873
+ alpha = alphaKeys[alphaKeys.length - 1].alpha;
874
+ }
875
+ else {
876
+ for (let i = 0; i < alphaKeys.length - 1; i++) {
877
+ if (t >= alphaKeys[i].time && t <= alphaKeys[i + 1].time) {
878
+ const localT = (t - alphaKeys[i].time) / (alphaKeys[i + 1].time - alphaKeys[i].time);
879
+ alpha = alphaKeys[i].alpha + (alphaKeys[i + 1].alpha - alphaKeys[i].alpha) * localT;
880
+ break;
881
+ }
882
+ }
883
+ }
884
+ }
885
+ return [color[0], color[1], color[2], alpha];
886
+ }
887
+ evaluateMinMaxVector3(vec, t, seed) {
888
+ switch (vec.mode) {
889
+ case 'constant':
890
+ return vec.value;
891
+ case 'randomBetweenTwoConstants':
892
+ const rand = this.seededRandom(seed);
893
+ return [
894
+ vec.min[0] + (vec.max[0] - vec.min[0]) * rand,
895
+ vec.min[1] + (vec.max[1] - vec.min[1]) * rand,
896
+ vec.min[2] + (vec.max[2] - vec.min[2]) * rand,
897
+ ];
898
+ }
899
+ }
900
+ seededRandom(seed) {
901
+ const x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
902
+ return x - Math.floor(x);
903
+ }
904
+ normalizedTime() {
905
+ return this.state.time / this.definition.main.duration;
906
+ }
907
+ // ─── Public API ────────────────────────────────────────
908
+ get particleCount() {
909
+ return this.state.activeCount;
910
+ }
911
+ get isPlaying() {
912
+ return this.state.isPlaying;
913
+ }
914
+ get isPaused() {
915
+ return this.state.isPaused;
916
+ }
917
+ get isStopped() {
918
+ return this.state.isStopped;
919
+ }
920
+ get time() {
921
+ return this.state.time;
922
+ }
923
+ get duration() {
924
+ return this.definition.main.duration;
925
+ }
926
+ getParticles() {
927
+ return this.state.particles.filter((p) => p.remainingLifetime >= 0);
928
+ }
929
+ getDefinition() {
930
+ return this.definition;
931
+ }
932
+ setDefinition(def) {
933
+ this.definition = def;
934
+ this.state = this.createInitialState();
935
+ }
936
+ }
937
+ // ============================================================
938
+ // Factory Functions
939
+ // ============================================================
940
+ function createDefaultParticleSystem(name) {
941
+ return {
942
+ id: `ps_${Date.now()}`,
943
+ name,
944
+ main: {
945
+ duration: 5,
946
+ looping: true,
947
+ prewarm: false,
948
+ startDelay: { mode: 'constant', value: 0 },
949
+ startLifetime: { mode: 'constant', value: 5 },
950
+ startSpeed: { mode: 'constant', value: 5 },
951
+ startSize3D: false,
952
+ startSize: { mode: 'constant', value: 1 },
953
+ startRotation3D: false,
954
+ startRotation: { mode: 'constant', value: 0 },
955
+ flipRotation: 0,
956
+ startColor: { mode: 'color', color: [1, 1, 1, 1] },
957
+ gravityModifier: { mode: 'constant', value: 0 },
958
+ simulationSpace: 'local',
959
+ simulationSpeed: 1,
960
+ deltaTime: 'scaled',
961
+ scalingMode: 'local',
962
+ playOnAwake: true,
963
+ emitterVelocityMode: 'transform',
964
+ maxParticles: 1000,
965
+ stopAction: 'none',
966
+ cullingMode: 'automatic',
967
+ ringBufferMode: 'disabled',
968
+ ringBufferLoopRange: [0, 1],
969
+ },
970
+ emission: {
971
+ enabled: true,
972
+ rateOverTime: { mode: 'constant', value: 10 },
973
+ rateOverDistance: { mode: 'constant', value: 0 },
974
+ bursts: [],
975
+ },
976
+ shape: {
977
+ type: 'cone',
978
+ angle: 25,
979
+ radius: 1,
980
+ radiusThickness: 1,
981
+ arc: 360,
982
+ arcMode: 'random',
983
+ arcSpeed: 1,
984
+ length: 5,
985
+ emitFrom: 'base',
986
+ position: [0, 0, 0],
987
+ rotation: [0, 0, 0],
988
+ scale: [1, 1, 1],
989
+ },
990
+ velocityOverLifetime: {
991
+ enabled: false,
992
+ space: 'local',
993
+ linear: { mode: 'constant', value: [0, 0, 0] },
994
+ orbital: { mode: 'constant', value: [0, 0, 0] },
995
+ offset: { mode: 'constant', value: [0, 0, 0] },
996
+ radial: { mode: 'constant', value: 0 },
997
+ speedModifier: { mode: 'constant', value: 1 },
998
+ },
999
+ limitVelocityOverLifetime: {
1000
+ enabled: false,
1001
+ separateAxes: false,
1002
+ speed: { mode: 'constant', value: 1 },
1003
+ space: 'local',
1004
+ dampen: 0.1,
1005
+ drag: { mode: 'constant', value: 0 },
1006
+ multiplyDragBySize: false,
1007
+ multiplyDragByVelocity: false,
1008
+ },
1009
+ inheritVelocity: {
1010
+ enabled: false,
1011
+ mode: 'initial',
1012
+ curve: { mode: 'constant', value: 0 },
1013
+ },
1014
+ lifetimeByEmitterSpeed: {
1015
+ enabled: false,
1016
+ curve: { mode: 'constant', value: 1 },
1017
+ speedRange: [0, 1],
1018
+ },
1019
+ forceOverLifetime: {
1020
+ enabled: false,
1021
+ space: 'local',
1022
+ x: { mode: 'constant', value: 0 },
1023
+ y: { mode: 'constant', value: 0 },
1024
+ z: { mode: 'constant', value: 0 },
1025
+ randomize: false,
1026
+ },
1027
+ colorOverLifetime: {
1028
+ enabled: false,
1029
+ color: { mode: 'color', color: [1, 1, 1, 1] },
1030
+ },
1031
+ colorBySpeed: {
1032
+ enabled: false,
1033
+ color: { mode: 'color', color: [1, 1, 1, 1] },
1034
+ speedRange: [0, 1],
1035
+ },
1036
+ sizeOverLifetime: {
1037
+ enabled: false,
1038
+ separateAxes: false,
1039
+ size: { mode: 'constant', value: 1 },
1040
+ },
1041
+ sizeBySpeed: {
1042
+ enabled: false,
1043
+ separateAxes: false,
1044
+ size: { mode: 'constant', value: 1 },
1045
+ speedRange: [0, 1],
1046
+ },
1047
+ rotationOverLifetime: {
1048
+ enabled: false,
1049
+ separateAxes: false,
1050
+ angularVelocity: { mode: 'constant', value: 0 },
1051
+ },
1052
+ rotationBySpeed: {
1053
+ enabled: false,
1054
+ separateAxes: false,
1055
+ angularVelocity: { mode: 'constant', value: 0 },
1056
+ speedRange: [0, 1],
1057
+ },
1058
+ externalForces: {
1059
+ enabled: false,
1060
+ multiplier: { mode: 'constant', value: 1 },
1061
+ influenceFilter: 'layerMask',
1062
+ influenceMask: 0xffffffff,
1063
+ forceFields: [],
1064
+ },
1065
+ noise: {
1066
+ enabled: false,
1067
+ separateAxes: false,
1068
+ strength: { mode: 'constant', value: 1 },
1069
+ frequency: 0.5,
1070
+ scrollSpeed: 0,
1071
+ damping: true,
1072
+ octaves: 1,
1073
+ octaveMultiplier: 0.5,
1074
+ octaveScale: 2,
1075
+ quality: 'medium',
1076
+ remapEnabled: false,
1077
+ remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
1078
+ positionAmount: { mode: 'constant', value: 1 },
1079
+ rotationAmount: { mode: 'constant', value: 0 },
1080
+ sizeAmount: { mode: 'constant', value: 0 },
1081
+ },
1082
+ collision: {
1083
+ enabled: false,
1084
+ type: 'planes',
1085
+ planes: [{ normal: [0, 1, 0], position: [0, 0, 0] }],
1086
+ bounce: { mode: 'constant', value: 0.5 },
1087
+ lifetimeLoss: { mode: 'constant', value: 0 },
1088
+ minKillSpeed: 0,
1089
+ radiusScale: 1,
1090
+ dampen: { mode: 'constant', value: 0 },
1091
+ enableDynamicColliders: false,
1092
+ colliderForce: 0,
1093
+ multiplyByCollisionAngle: true,
1094
+ multiplyByParticleSpeed: true,
1095
+ multiplyByParticleSize: false,
1096
+ sendCollisionMessages: false,
1097
+ },
1098
+ triggers: {
1099
+ enabled: false,
1100
+ inside: 'ignore',
1101
+ outside: 'ignore',
1102
+ enter: 'ignore',
1103
+ exit: 'ignore',
1104
+ colliderQueryMode: 'disabled',
1105
+ radiusScale: 1,
1106
+ },
1107
+ subEmitters: [],
1108
+ textureSheetAnimation: {
1109
+ enabled: false,
1110
+ mode: 'grid',
1111
+ tilesX: 1,
1112
+ tilesY: 1,
1113
+ animation: 'wholeSheet',
1114
+ rowIndex: 0,
1115
+ rowMode: 'custom',
1116
+ timeMode: 'lifetime',
1117
+ frameOverTime: { mode: 'constant', value: 0 },
1118
+ startFrame: { mode: 'constant', value: 0 },
1119
+ cycleCount: 1,
1120
+ speedRange: [0, 1],
1121
+ fps: 30,
1122
+ sprites: [],
1123
+ },
1124
+ lights: {
1125
+ enabled: false,
1126
+ ratio: 1,
1127
+ randomDistribution: true,
1128
+ light: {
1129
+ type: 'point',
1130
+ color: { mode: 'color', color: [1, 1, 1, 1] },
1131
+ intensity: { mode: 'constant', value: 1 },
1132
+ range: { mode: 'constant', value: 1 },
1133
+ },
1134
+ useParticleColor: true,
1135
+ sizeAffectsRange: true,
1136
+ alphaAffectsIntensity: true,
1137
+ maxLights: 20,
1138
+ },
1139
+ trails: {
1140
+ enabled: false,
1141
+ ratio: 1,
1142
+ lifetime: { mode: 'constant', value: 1 },
1143
+ minVertexDistance: 0.2,
1144
+ textureMode: 'stretch',
1145
+ worldSpace: false,
1146
+ dieWithParticle: true,
1147
+ sizeAffectsWidth: true,
1148
+ sizeAffectsLifetime: false,
1149
+ inheritParticleColor: true,
1150
+ colorOverLifetime: { mode: 'color', color: [1, 1, 1, 1] },
1151
+ widthOverTrail: { mode: 'constant', value: 1 },
1152
+ colorOverTrail: { mode: 'color', color: [1, 1, 1, 1] },
1153
+ generateLightingData: false,
1154
+ shadowBias: 0.5,
1155
+ },
1156
+ customData: {
1157
+ enabled: false,
1158
+ stream1: { mode: 'disabled', components: 4 },
1159
+ stream2: { mode: 'disabled', components: 4 },
1160
+ },
1161
+ renderer: {
1162
+ enabled: true,
1163
+ renderMode: 'billboard',
1164
+ material: '',
1165
+ sortMode: 'none',
1166
+ sortingFudge: 0,
1167
+ minParticleSize: 0,
1168
+ maxParticleSize: 0.5,
1169
+ cameraVelocityScale: 0,
1170
+ velocityScale: 0,
1171
+ lengthScale: 2,
1172
+ normalDirection: 1,
1173
+ shadowCastingMode: 'off',
1174
+ receiveShadows: false,
1175
+ motionVectors: 'cameraMotionOnly',
1176
+ lightProbes: 'off',
1177
+ reflectionProbes: 'off',
1178
+ maskInteraction: 'none',
1179
+ pivot: [0, 0, 0],
1180
+ flip: [0, 0, 0],
1181
+ allowRoll: true,
1182
+ freeformStretching: false,
1183
+ rotateWithStretchDirection: true,
1184
+ enableGPUInstancing: false,
1185
+ },
1186
+ };
1187
+ }
1188
+ function createParticleSystem(def) {
1189
+ return new ParticleSystem2(def);
1190
+ }
1191
+ // ============================================================
1192
+ // Preset Systems
1193
+ // ============================================================
1194
+ function createFireParticleSystem() {
1195
+ const def = createDefaultParticleSystem('Fire');
1196
+ def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 0.5, max: 1.5 };
1197
+ def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.1, max: 0.5 };
1198
+ def.main.startColor = {
1199
+ mode: 'randomBetweenTwoColors',
1200
+ colorMin: [1, 0.5, 0, 1],
1201
+ colorMax: [1, 0.2, 0, 1],
1202
+ };
1203
+ def.main.gravityModifier = { mode: 'constant', value: -0.5 };
1204
+ def.emission.rateOverTime = { mode: 'constant', value: 50 };
1205
+ def.shape = {
1206
+ type: 'cone',
1207
+ angle: 15,
1208
+ radius: 0.2,
1209
+ radiusThickness: 1,
1210
+ arc: 360,
1211
+ arcMode: 'random',
1212
+ arcSpeed: 1,
1213
+ length: 1,
1214
+ emitFrom: 'base',
1215
+ position: [0, 0, 0],
1216
+ rotation: [0, 0, 0],
1217
+ scale: [1, 1, 1],
1218
+ };
1219
+ def.colorOverLifetime = {
1220
+ enabled: true,
1221
+ color: {
1222
+ mode: 'gradient',
1223
+ gradient: {
1224
+ colorKeys: [
1225
+ { time: 0, color: [1, 0.8, 0] },
1226
+ { time: 0.3, color: [1, 0.3, 0] },
1227
+ { time: 1, color: [0.2, 0, 0] },
1228
+ ],
1229
+ alphaKeys: [
1230
+ { time: 0, alpha: 1 },
1231
+ { time: 0.7, alpha: 0.8 },
1232
+ { time: 1, alpha: 0 },
1233
+ ],
1234
+ mode: 'blend',
1235
+ },
1236
+ },
1237
+ };
1238
+ def.sizeOverLifetime = {
1239
+ enabled: true,
1240
+ separateAxes: false,
1241
+ size: {
1242
+ mode: 'curve',
1243
+ curve: {
1244
+ keyframes: [
1245
+ { time: 0, value: 0.5, inTangent: 0, outTangent: 1, mode: 'auto' },
1246
+ { time: 0.3, value: 1, inTangent: 0, outTangent: 0, mode: 'auto' },
1247
+ { time: 1, value: 0, inTangent: -1, outTangent: 0, mode: 'auto' },
1248
+ ],
1249
+ preWrapMode: 'clamp',
1250
+ postWrapMode: 'clamp',
1251
+ },
1252
+ multiplier: 1,
1253
+ },
1254
+ };
1255
+ def.noise = {
1256
+ enabled: true,
1257
+ separateAxes: false,
1258
+ strength: { mode: 'constant', value: 0.3 },
1259
+ frequency: 1,
1260
+ scrollSpeed: 1,
1261
+ damping: true,
1262
+ octaves: 2,
1263
+ octaveMultiplier: 0.5,
1264
+ octaveScale: 2,
1265
+ quality: 'medium',
1266
+ remapEnabled: false,
1267
+ remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
1268
+ positionAmount: { mode: 'constant', value: 1 },
1269
+ rotationAmount: { mode: 'constant', value: 0 },
1270
+ sizeAmount: { mode: 'constant', value: 0 },
1271
+ };
1272
+ def.renderer.renderMode = 'billboard';
1273
+ return def;
1274
+ }
1275
+ function createSmokeParticleSystem() {
1276
+ const def = createDefaultParticleSystem('Smoke');
1277
+ def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 3, max: 5 };
1278
+ def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.5, max: 1 };
1279
+ def.main.startColor = {
1280
+ mode: 'randomBetweenTwoColors',
1281
+ colorMin: [0.5, 0.5, 0.5, 0.5],
1282
+ colorMax: [0.3, 0.3, 0.3, 0.3],
1283
+ };
1284
+ def.main.gravityModifier = { mode: 'constant', value: -0.1 };
1285
+ def.emission.rateOverTime = { mode: 'constant', value: 20 };
1286
+ def.shape = {
1287
+ type: 'circle',
1288
+ radius: 0.3,
1289
+ radiusThickness: 1,
1290
+ arc: 360,
1291
+ arcMode: 'random',
1292
+ arcSpeed: 1,
1293
+ position: [0, 0, 0],
1294
+ rotation: [0, 0, 0],
1295
+ scale: [1, 1, 1],
1296
+ };
1297
+ def.sizeOverLifetime = {
1298
+ enabled: true,
1299
+ separateAxes: false,
1300
+ size: {
1301
+ mode: 'curve',
1302
+ curve: {
1303
+ keyframes: [
1304
+ { time: 0, value: 0.3, inTangent: 0, outTangent: 2, mode: 'auto' },
1305
+ { time: 1, value: 3, inTangent: 0.5, outTangent: 0, mode: 'auto' },
1306
+ ],
1307
+ preWrapMode: 'clamp',
1308
+ postWrapMode: 'clamp',
1309
+ },
1310
+ multiplier: 1,
1311
+ },
1312
+ };
1313
+ def.colorOverLifetime = {
1314
+ enabled: true,
1315
+ color: {
1316
+ mode: 'gradient',
1317
+ gradient: {
1318
+ colorKeys: [
1319
+ { time: 0, color: [0.4, 0.4, 0.4] },
1320
+ { time: 1, color: [0.6, 0.6, 0.6] },
1321
+ ],
1322
+ alphaKeys: [
1323
+ { time: 0, alpha: 0 },
1324
+ { time: 0.1, alpha: 0.5 },
1325
+ { time: 0.8, alpha: 0.3 },
1326
+ { time: 1, alpha: 0 },
1327
+ ],
1328
+ mode: 'blend',
1329
+ },
1330
+ },
1331
+ };
1332
+ def.rotationOverLifetime = {
1333
+ enabled: true,
1334
+ separateAxes: false,
1335
+ angularVelocity: { mode: 'randomBetweenTwoConstants', min: -0.5, max: 0.5 },
1336
+ };
1337
+ def.noise = {
1338
+ enabled: true,
1339
+ separateAxes: false,
1340
+ strength: { mode: 'constant', value: 0.5 },
1341
+ frequency: 0.3,
1342
+ scrollSpeed: 0.2,
1343
+ damping: true,
1344
+ octaves: 2,
1345
+ octaveMultiplier: 0.5,
1346
+ octaveScale: 2,
1347
+ quality: 'medium',
1348
+ remapEnabled: false,
1349
+ remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
1350
+ positionAmount: { mode: 'constant', value: 1 },
1351
+ rotationAmount: { mode: 'constant', value: 0 },
1352
+ sizeAmount: { mode: 'constant', value: 0 },
1353
+ };
1354
+ return def;
1355
+ }
1356
+ function createSparkParticleSystem() {
1357
+ const def = createDefaultParticleSystem('Sparks');
1358
+ def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 0.2, max: 0.5 };
1359
+ def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.02, max: 0.05 };
1360
+ def.main.startSpeed = { mode: 'randomBetweenTwoConstants', min: 3, max: 8 };
1361
+ def.main.startColor = {
1362
+ mode: 'randomBetweenTwoColors',
1363
+ colorMin: [1, 0.8, 0, 1],
1364
+ colorMax: [1, 0.5, 0, 1],
1365
+ };
1366
+ def.main.gravityModifier = { mode: 'constant', value: 1 };
1367
+ def.emission.rateOverTime = { mode: 'constant', value: 0 };
1368
+ def.emission.bursts = [
1369
+ {
1370
+ time: 0,
1371
+ count: { mode: 'randomBetweenTwoConstants', min: 10, max: 30 },
1372
+ cycles: 1,
1373
+ interval: 0.01,
1374
+ probability: 1,
1375
+ },
1376
+ ];
1377
+ def.shape = {
1378
+ type: 'sphere',
1379
+ radius: 0.1,
1380
+ radiusThickness: 0,
1381
+ arc: 360,
1382
+ arcMode: 'random',
1383
+ arcSpeed: 1,
1384
+ position: [0, 0, 0],
1385
+ rotation: [0, 0, 0],
1386
+ scale: [1, 1, 1],
1387
+ };
1388
+ def.colorOverLifetime = {
1389
+ enabled: true,
1390
+ color: {
1391
+ mode: 'gradient',
1392
+ gradient: {
1393
+ colorKeys: [
1394
+ { time: 0, color: [1, 0.9, 0.5] },
1395
+ { time: 0.5, color: [1, 0.5, 0] },
1396
+ { time: 1, color: [0.5, 0, 0] },
1397
+ ],
1398
+ alphaKeys: [
1399
+ { time: 0, alpha: 1 },
1400
+ { time: 0.8, alpha: 1 },
1401
+ { time: 1, alpha: 0 },
1402
+ ],
1403
+ mode: 'blend',
1404
+ },
1405
+ },
1406
+ };
1407
+ def.trails = {
1408
+ enabled: true,
1409
+ ratio: 1,
1410
+ lifetime: { mode: 'constant', value: 0.1 },
1411
+ minVertexDistance: 0.01,
1412
+ textureMode: 'stretch',
1413
+ worldSpace: true,
1414
+ dieWithParticle: true,
1415
+ sizeAffectsWidth: true,
1416
+ sizeAffectsLifetime: false,
1417
+ inheritParticleColor: true,
1418
+ colorOverLifetime: { mode: 'color', color: [1, 1, 1, 1] },
1419
+ widthOverTrail: {
1420
+ mode: 'curve',
1421
+ curve: {
1422
+ keyframes: [
1423
+ { time: 0, value: 1, inTangent: 0, outTangent: 0, mode: 'auto' },
1424
+ { time: 1, value: 0, inTangent: -1, outTangent: 0, mode: 'auto' },
1425
+ ],
1426
+ preWrapMode: 'clamp',
1427
+ postWrapMode: 'clamp',
1428
+ },
1429
+ multiplier: 1,
1430
+ },
1431
+ colorOverTrail: {
1432
+ mode: 'gradient',
1433
+ gradient: {
1434
+ colorKeys: [
1435
+ { time: 0, color: [1, 0.8, 0] },
1436
+ { time: 1, color: [1, 0.2, 0] },
1437
+ ],
1438
+ alphaKeys: [
1439
+ { time: 0, alpha: 1 },
1440
+ { time: 1, alpha: 0 },
1441
+ ],
1442
+ mode: 'blend',
1443
+ },
1444
+ },
1445
+ generateLightingData: false,
1446
+ shadowBias: 0.5,
1447
+ };
1448
+ def.collision = {
1449
+ enabled: true,
1450
+ type: 'planes',
1451
+ planes: [{ normal: [0, 1, 0], position: [0, 0, 0] }],
1452
+ bounce: { mode: 'randomBetweenTwoConstants', min: 0.2, max: 0.6 },
1453
+ lifetimeLoss: { mode: 'constant', value: 0.2 },
1454
+ minKillSpeed: 0.1,
1455
+ radiusScale: 1,
1456
+ dampen: { mode: 'constant', value: 0.3 },
1457
+ enableDynamicColliders: false,
1458
+ colliderForce: 0,
1459
+ multiplyByCollisionAngle: true,
1460
+ multiplyByParticleSpeed: true,
1461
+ multiplyByParticleSize: false,
1462
+ sendCollisionMessages: false,
1463
+ };
1464
+ def.renderer.renderMode = 'stretchedBillboard';
1465
+ def.renderer.velocityScale = 0.1;
1466
+ def.renderer.lengthScale = 5;
1467
+ return def;
1468
+ }
1469
+
1470
+ export { ParticleSystem2, createDefaultParticleSystem, createFireParticleSystem, createParticleSystem, createSmokeParticleSystem, createSparkParticleSystem };
1471
+ //# sourceMappingURL=ParticleSystem2.js.map