@nice2dev/ui-3d 1.3.14 → 1.3.16

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 (108) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,1090 +1,1105 @@
1
- import { DEFAULT_CURVE, DEFAULT_GRADIENT } from './particleEditorTypes.js';
2
-
3
- /* ════════════════════════════════════════════════════════════════════════════
4
- Particle System Editor — Utility Functions
5
-
6
- Core utilities for particle system creation, manipulation, and simulation
7
- ════════════════════════════════════════════════════════════════════════════ */
8
- // ─── ID Generation ───────────────────────────────────────────────────────────
1
+ import { DEFAULT_CURVE, DEFAULT_GRADIENT } from "./particleEditorTypes.js";
9
2
  let systemIdCounter = 0;
10
3
  let forceIdCounter = 0;
11
4
  let subEmitterIdCounter = 0;
12
5
  function generateSystemId() {
13
- return `system_${++systemIdCounter}_${Date.now()}`;
6
+ return `system_${++systemIdCounter}_${Date.now()}`;
14
7
  }
15
8
  function generateForceId() {
16
- return `force_${++forceIdCounter}_${Date.now()}`;
9
+ return `force_${++forceIdCounter}_${Date.now()}`;
17
10
  }
18
11
  function generateSubEmitterId() {
19
- return `subemitter_${++subEmitterIdCounter}_${Date.now()}`;
12
+ return `subemitter_${++subEmitterIdCounter}_${Date.now()}`;
20
13
  }
21
- // ─── Distribution Operations ─────────────────────────────────────────────────
22
14
  function evaluateDistribution(dist, t, seed = Math.random()) {
23
- var _a, _b;
24
- switch (dist.type) {
25
- case 'constant':
26
- return dist.value;
27
- case 'random':
28
- return dist.min + seed * (dist.max - dist.min);
29
- case 'curve':
30
- return evaluateCurve(dist.curve, t) * ((_a = dist.multiplier) !== null && _a !== void 0 ? _a : 1);
31
- case 'random_curve': {
32
- const min = evaluateCurve(dist.curveMin, t);
33
- const max = evaluateCurve(dist.curveMax, t);
34
- return (min + seed * (max - min)) * ((_b = dist.multiplier) !== null && _b !== void 0 ? _b : 1);
35
- }
36
- default:
37
- return 1;
15
+ switch (dist.type) {
16
+ case "constant":
17
+ return dist.value;
18
+ case "random":
19
+ return dist.min + seed * (dist.max - dist.min);
20
+ case "curve":
21
+ return evaluateCurve(dist.curve, t) * (dist.multiplier ?? 1);
22
+ case "random_curve": {
23
+ const min = evaluateCurve(dist.curveMin, t);
24
+ const max = evaluateCurve(dist.curveMax, t);
25
+ return (min + seed * (max - min)) * (dist.multiplier ?? 1);
38
26
  }
27
+ default:
28
+ return 1;
29
+ }
39
30
  }
40
31
  function evaluateCurve(curve, t) {
41
- var _a, _b;
42
- if (curve.length === 0) {
43
- return 0;
44
- }
45
- if (curve.length === 1) {
46
- return curve[0].value;
32
+ if (curve.length === 0) {
33
+ return 0;
34
+ }
35
+ if (curve.length === 1) {
36
+ return curve[0].value;
37
+ }
38
+ t = Math.max(0, Math.min(1, t));
39
+ let leftIdx = 0;
40
+ let rightIdx = curve.length - 1;
41
+ for (let i = 0; i < curve.length; i++) {
42
+ if (curve[i].time <= t) {
43
+ leftIdx = i;
47
44
  }
48
- // Clamp t to [0, 1]
49
- t = Math.max(0, Math.min(1, t));
50
- // Find surrounding keyframes
51
- let leftIdx = 0;
52
- let rightIdx = curve.length - 1;
53
- for (let i = 0; i < curve.length; i++) {
54
- if (curve[i].time <= t) {
55
- leftIdx = i;
56
- }
57
- if (curve[i].time >= t && i < rightIdx) {
58
- rightIdx = i;
59
- }
60
- }
61
- if (leftIdx === rightIdx) {
62
- return curve[leftIdx].value;
45
+ if (curve[i].time >= t && i < rightIdx) {
46
+ rightIdx = i;
63
47
  }
64
- const left = curve[leftIdx];
65
- const right = curve[rightIdx];
66
- // Hermite interpolation
67
- const dt = right.time - left.time;
68
- const localT = (t - left.time) / dt;
69
- const outTangent = (_a = left.outTangent) !== null && _a !== void 0 ? _a : 0;
70
- const inTangent = (_b = right.inTangent) !== null && _b !== void 0 ? _b : 0;
71
- const t2 = localT * localT;
72
- const t3 = t2 * localT;
73
- const h00 = 2 * t3 - 3 * t2 + 1;
74
- const h10 = t3 - 2 * t2 + localT;
75
- const h01 = -2 * t3 + 3 * t2;
76
- const h11 = t3 - t2;
77
- return h00 * left.value + h10 * dt * outTangent + h01 * right.value + h11 * dt * inTangent;
48
+ }
49
+ if (leftIdx === rightIdx) {
50
+ return curve[leftIdx].value;
51
+ }
52
+ const left = curve[leftIdx];
53
+ const right = curve[rightIdx];
54
+ const dt = right.time - left.time;
55
+ const localT = (t - left.time) / dt;
56
+ const outTangent = left.outTangent ?? 0;
57
+ const inTangent = right.inTangent ?? 0;
58
+ const t2 = localT * localT;
59
+ const t3 = t2 * localT;
60
+ const h00 = 2 * t3 - 3 * t2 + 1;
61
+ const h10 = t3 - 2 * t2 + localT;
62
+ const h01 = -2 * t3 + 3 * t2;
63
+ const h11 = t3 - t2;
64
+ return h00 * left.value + h10 * dt * outTangent + h01 * right.value + h11 * dt * inTangent;
78
65
  }
79
66
  function evaluateVec3Distribution(dist, t, seed) {
80
- return {
81
- x: evaluateDistribution(dist.x, t, seed.x),
82
- y: evaluateDistribution(dist.y, t, seed.y),
83
- z: evaluateDistribution(dist.z, t, seed.z),
84
- };
67
+ return {
68
+ x: evaluateDistribution(dist.x, t, seed.x),
69
+ y: evaluateDistribution(dist.y, t, seed.y),
70
+ z: evaluateDistribution(dist.z, t, seed.z)
71
+ };
85
72
  }
86
73
  function evaluateGradient(gradient, t) {
87
- // Interpolate color
88
- let color = { r: 1, g: 1, b: 1 };
89
- const colorKeys = gradient.colorKeys;
90
- if (colorKeys.length > 0) {
91
- let leftIdx = 0;
92
- let rightIdx = colorKeys.length - 1;
93
- for (let i = 0; i < colorKeys.length; i++) {
94
- if (colorKeys[i].time <= t) {
95
- leftIdx = i;
96
- }
97
- if (colorKeys[i].time >= t && i < rightIdx) {
98
- rightIdx = i;
99
- }
100
- }
101
- if (leftIdx === rightIdx) {
102
- color = { ...colorKeys[leftIdx].color };
103
- }
104
- else {
105
- const left = colorKeys[leftIdx];
106
- const right = colorKeys[rightIdx];
107
- const localT = (t - left.time) / (right.time - left.time);
108
- color = {
109
- r: left.color.r + localT * (right.color.r - left.color.r),
110
- g: left.color.g + localT * (right.color.g - left.color.g),
111
- b: left.color.b + localT * (right.color.b - left.color.b),
112
- };
113
- }
74
+ let color = { r: 1, g: 1, b: 1 };
75
+ const colorKeys = gradient.colorKeys;
76
+ if (colorKeys.length > 0) {
77
+ let leftIdx = 0;
78
+ let rightIdx = colorKeys.length - 1;
79
+ for (let i = 0; i < colorKeys.length; i++) {
80
+ if (colorKeys[i].time <= t) {
81
+ leftIdx = i;
82
+ }
83
+ if (colorKeys[i].time >= t && i < rightIdx) {
84
+ rightIdx = i;
85
+ }
114
86
  }
115
- // Interpolate alpha
116
- let alpha = 1;
117
- const alphaKeys = gradient.alphaKeys;
118
- if (alphaKeys.length > 0) {
119
- let leftIdx = 0;
120
- let rightIdx = alphaKeys.length - 1;
121
- for (let i = 0; i < alphaKeys.length; i++) {
122
- if (alphaKeys[i].time <= t) {
123
- leftIdx = i;
124
- }
125
- if (alphaKeys[i].time >= t && i < rightIdx) {
126
- rightIdx = i;
127
- }
128
- }
129
- if (leftIdx === rightIdx) {
130
- alpha = alphaKeys[leftIdx].alpha;
131
- }
132
- else {
133
- const left = alphaKeys[leftIdx];
134
- const right = alphaKeys[rightIdx];
135
- const localT = (t - left.time) / (right.time - left.time);
136
- alpha = left.alpha + localT * (right.alpha - left.alpha);
137
- }
87
+ if (leftIdx === rightIdx) {
88
+ color = { ...colorKeys[leftIdx].color };
89
+ } else {
90
+ const left = colorKeys[leftIdx];
91
+ const right = colorKeys[rightIdx];
92
+ const localT = (t - left.time) / (right.time - left.time);
93
+ color = {
94
+ r: left.color.r + localT * (right.color.r - left.color.r),
95
+ g: left.color.g + localT * (right.color.g - left.color.g),
96
+ b: left.color.b + localT * (right.color.b - left.color.b)
97
+ };
98
+ }
99
+ }
100
+ let alpha = 1;
101
+ const alphaKeys = gradient.alphaKeys;
102
+ if (alphaKeys.length > 0) {
103
+ let leftIdx = 0;
104
+ let rightIdx = alphaKeys.length - 1;
105
+ for (let i = 0; i < alphaKeys.length; i++) {
106
+ if (alphaKeys[i].time <= t) {
107
+ leftIdx = i;
108
+ }
109
+ if (alphaKeys[i].time >= t && i < rightIdx) {
110
+ rightIdx = i;
111
+ }
138
112
  }
139
- return { ...color, a: alpha };
113
+ if (leftIdx === rightIdx) {
114
+ alpha = alphaKeys[leftIdx].alpha;
115
+ } else {
116
+ const left = alphaKeys[leftIdx];
117
+ const right = alphaKeys[rightIdx];
118
+ const localT = (t - left.time) / (right.time - left.time);
119
+ alpha = left.alpha + localT * (right.alpha - left.alpha);
120
+ }
121
+ }
122
+ return { ...color, a: alpha };
140
123
  }
141
- // ─── Vector Operations ───────────────────────────────────────────────────────
142
124
  function vec3Add(a, b) {
143
- return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
125
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
144
126
  }
145
127
  function vec3Sub(a, b) {
146
- return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
128
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
147
129
  }
148
130
  function vec3Mul(a, s) {
149
- return { x: a.x * s, y: a.y * s, z: a.z * s };
131
+ return { x: a.x * s, y: a.y * s, z: a.z * s };
150
132
  }
151
133
  function vec3MulVec(a, b) {
152
- return { x: a.x * b.x, y: a.y * b.y, z: a.z * b.z };
134
+ return { x: a.x * b.x, y: a.y * b.y, z: a.z * b.z };
153
135
  }
154
136
  function vec3Dot(a, b) {
155
- return a.x * b.x + a.y * b.y + a.z * b.z;
137
+ return a.x * b.x + a.y * b.y + a.z * b.z;
156
138
  }
157
139
  function vec3Cross(a, b) {
158
- return {
159
- x: a.y * b.z - a.z * b.y,
160
- y: a.z * b.x - a.x * b.z,
161
- z: a.x * b.y - a.y * b.x,
162
- };
140
+ return {
141
+ x: a.y * b.z - a.z * b.y,
142
+ y: a.z * b.x - a.x * b.z,
143
+ z: a.x * b.y - a.y * b.x
144
+ };
163
145
  }
164
146
  function vec3Length(v) {
165
- return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
147
+ return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
166
148
  }
167
149
  function vec3Normalize(v) {
168
- const len = vec3Length(v);
169
- if (len === 0) {
170
- return { x: 0, y: 0, z: 0 };
171
- }
172
- return { x: v.x / len, y: v.y / len, z: v.z / len };
150
+ const len = vec3Length(v);
151
+ if (len === 0) {
152
+ return { x: 0, y: 0, z: 0 };
153
+ }
154
+ return { x: v.x / len, y: v.y / len, z: v.z / len };
173
155
  }
174
156
  function vec3Lerp(a, b, t) {
175
- return {
176
- x: a.x + t * (b.x - a.x),
177
- y: a.y + t * (b.y - a.y),
178
- z: a.z + t * (b.z - a.z),
179
- };
157
+ return {
158
+ x: a.x + t * (b.x - a.x),
159
+ y: a.y + t * (b.y - a.y),
160
+ z: a.z + t * (b.z - a.z)
161
+ };
180
162
  }
181
163
  function vec3Random() {
182
- return {
183
- x: Math.random() * 2 - 1,
184
- y: Math.random() * 2 - 1,
185
- z: Math.random() * 2 - 1,
186
- };
164
+ return {
165
+ x: Math.random() * 2 - 1,
166
+ y: Math.random() * 2 - 1,
167
+ z: Math.random() * 2 - 1
168
+ };
187
169
  }
188
170
  function vec3RandomOnSphere() {
189
- const theta = Math.random() * Math.PI * 2;
190
- const phi = Math.acos(Math.random() * 2 - 1);
191
- return {
192
- x: Math.sin(phi) * Math.cos(theta),
193
- y: Math.sin(phi) * Math.sin(theta),
194
- z: Math.cos(phi),
195
- };
171
+ const theta = Math.random() * Math.PI * 2;
172
+ const phi = Math.acos(Math.random() * 2 - 1);
173
+ return {
174
+ x: Math.sin(phi) * Math.cos(theta),
175
+ y: Math.sin(phi) * Math.sin(theta),
176
+ z: Math.cos(phi)
177
+ };
196
178
  }
197
- // ─── Random / Noise ──────────────────────────────────────────────────────────
198
179
  function seededRandom(seed) {
199
- const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453;
200
- return x - Math.floor(x);
180
+ const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453;
181
+ return x - Math.floor(x);
201
182
  }
202
183
  function perlinNoise2D(x, y) {
203
- // Simplified 2D Perlin noise
204
- const xi = Math.floor(x);
205
- const yi = Math.floor(y);
206
- const xf = x - xi;
207
- const yf = y - yi;
208
- const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
209
- const grad = (hash, x, y) => {
210
- const h = hash & 3;
211
- const u = h < 2 ? x : y;
212
- const v = h < 2 ? y : x;
213
- return (h & 1 ? -u : u) + (h & 2 ? -2 * v : 2 * v);
214
- };
215
- const hash = (x, y) => {
216
- const n = x * 374761393 + y * 668265263;
217
- return ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) >>> 0;
218
- };
219
- const u = fade(xf);
220
- const v = fade(yf);
221
- const aa = hash(xi, yi);
222
- const ab = hash(xi, yi + 1);
223
- const ba = hash(xi + 1, yi);
224
- const bb = hash(xi + 1, yi + 1);
225
- const x1 = grad(aa, xf, yf) * (1 - u) + grad(ba, xf - 1, yf) * u;
226
- const x2 = grad(ab, xf, yf - 1) * (1 - u) + grad(bb, xf - 1, yf - 1) * u;
227
- return (x1 * (1 - v) + x2 * v + 1) / 2;
184
+ const xi = Math.floor(x);
185
+ const yi = Math.floor(y);
186
+ const xf = x - xi;
187
+ const yf = y - yi;
188
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
189
+ const grad = (hash2, x3, y2) => {
190
+ const h = hash2 & 3;
191
+ const u2 = h < 2 ? x3 : y2;
192
+ const v2 = h < 2 ? y2 : x3;
193
+ return (h & 1 ? -u2 : u2) + (h & 2 ? -2 * v2 : 2 * v2);
194
+ };
195
+ const hash = (x3, y2) => {
196
+ const n = x3 * 374761393 + y2 * 668265263;
197
+ return (n * (n * n * 15731 + 789221) + 1376312589 & 2147483647) >>> 0;
198
+ };
199
+ const u = fade(xf);
200
+ const v = fade(yf);
201
+ const aa = hash(xi, yi);
202
+ const ab = hash(xi, yi + 1);
203
+ const ba = hash(xi + 1, yi);
204
+ const bb = hash(xi + 1, yi + 1);
205
+ const x1 = grad(aa, xf, yf) * (1 - u) + grad(ba, xf - 1, yf) * u;
206
+ const x2 = grad(ab, xf, yf - 1) * (1 - u) + grad(bb, xf - 1, yf - 1) * u;
207
+ return (x1 * (1 - v) + x2 * v + 1) / 2;
228
208
  }
229
209
  function fbmNoise(x, y, z, octaves, roughness) {
230
- let value = 0;
231
- let amplitude = 1;
232
- let frequency = 1;
233
- let maxValue = 0;
234
- for (let i = 0; i < octaves; i++) {
235
- value += amplitude * perlinNoise2D(x * frequency + z * 0.5, y * frequency);
236
- maxValue += amplitude;
237
- amplitude *= roughness;
238
- frequency *= 2;
239
- }
240
- return value / maxValue;
210
+ let value = 0;
211
+ let amplitude = 1;
212
+ let frequency = 1;
213
+ let maxValue = 0;
214
+ for (let i = 0; i < octaves; i++) {
215
+ value += amplitude * perlinNoise2D(x * frequency + z * 0.5, y * frequency);
216
+ maxValue += amplitude;
217
+ amplitude *= roughness;
218
+ frequency *= 2;
219
+ }
220
+ return value / maxValue;
241
221
  }
242
- // ─── Emitter Shape Sampling ──────────────────────────────────────────────────
243
222
  function sampleEmitterShape(shape, seed) {
244
- let position = { x: 0, y: 0, z: 0 };
245
- let direction = { x: 0, y: 1, z: 0 };
246
- switch (shape.type) {
247
- case 'point':
248
- position = { x: 0, y: 0, z: 0 };
249
- direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
223
+ let position = { x: 0, y: 0, z: 0 };
224
+ let direction = { x: 0, y: 1, z: 0 };
225
+ switch (shape.type) {
226
+ case "point":
227
+ position = { x: 0, y: 0, z: 0 };
228
+ direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
229
+ break;
230
+ case "sphere": {
231
+ const radius = evaluateDistribution(shape.radius, 0, seed.x);
232
+ const innerRadius = radius * (1 - shape.radiusThickness);
233
+ const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
234
+ const arc = shape.arc * Math.PI / 180;
235
+ const theta = seed.z * arc;
236
+ const phi = Math.acos(seed.x * 2 - 1);
237
+ position = {
238
+ x: r * Math.sin(phi) * Math.cos(theta),
239
+ y: r * Math.sin(phi) * Math.sin(theta),
240
+ z: r * Math.cos(phi)
241
+ };
242
+ direction = shape.sphericalDirection ? vec3Normalize(position) : { x: 0, y: 1, z: 0 };
243
+ break;
244
+ }
245
+ case "hemisphere": {
246
+ const radius = evaluateDistribution(shape.radius, 0, seed.x);
247
+ const innerRadius = radius * (1 - shape.radiusThickness);
248
+ const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
249
+ const arc = shape.arc * Math.PI / 180;
250
+ const theta = seed.z * arc;
251
+ const phi = Math.acos(seed.x);
252
+ position = {
253
+ x: r * Math.sin(phi) * Math.cos(theta),
254
+ y: r * Math.cos(phi),
255
+ z: r * Math.sin(phi) * Math.sin(theta)
256
+ };
257
+ direction = shape.sphericalDirection ? vec3Normalize(position) : { x: 0, y: 1, z: 0 };
258
+ break;
259
+ }
260
+ case "box": {
261
+ if (shape.emitFrom === "volume") {
262
+ position = {
263
+ x: (seed.x - 0.5) * shape.size.x,
264
+ y: (seed.y - 0.5) * shape.size.y,
265
+ z: (seed.z - 0.5) * shape.size.z
266
+ };
267
+ } else if (shape.emitFrom === "surface") {
268
+ const face = Math.floor(seed.x * 6);
269
+ const u = seed.y - 0.5;
270
+ const v = seed.z - 0.5;
271
+ switch (face) {
272
+ case 0:
273
+ position = { x: shape.size.x / 2, y: u * shape.size.y, z: v * shape.size.z };
250
274
  break;
251
- case 'sphere': {
252
- const radius = evaluateDistribution(shape.radius, 0, seed.x);
253
- const innerRadius = radius * (1 - shape.radiusThickness);
254
- const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
255
- const arc = (shape.arc * Math.PI) / 180;
256
- const theta = seed.z * arc;
257
- const phi = Math.acos(seed.x * 2 - 1);
258
- position = {
259
- x: r * Math.sin(phi) * Math.cos(theta),
260
- y: r * Math.sin(phi) * Math.sin(theta),
261
- z: r * Math.cos(phi),
262
- };
263
- direction = shape.sphericalDirection ? vec3Normalize(position) : { x: 0, y: 1, z: 0 };
275
+ case 1:
276
+ position = { x: -shape.size.x / 2, y: u * shape.size.y, z: v * shape.size.z };
264
277
  break;
265
- }
266
- case 'hemisphere': {
267
- const radius = evaluateDistribution(shape.radius, 0, seed.x);
268
- const innerRadius = radius * (1 - shape.radiusThickness);
269
- const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
270
- const arc = (shape.arc * Math.PI) / 180;
271
- const theta = seed.z * arc;
272
- const phi = Math.acos(seed.x); // Only upper hemisphere
273
- position = {
274
- x: r * Math.sin(phi) * Math.cos(theta),
275
- y: r * Math.cos(phi),
276
- z: r * Math.sin(phi) * Math.sin(theta),
277
- };
278
- direction = shape.sphericalDirection ? vec3Normalize(position) : { x: 0, y: 1, z: 0 };
278
+ case 2:
279
+ position = { x: u * shape.size.x, y: shape.size.y / 2, z: v * shape.size.z };
279
280
  break;
280
- }
281
- case 'box': {
282
- if (shape.emitFrom === 'volume') {
283
- position = {
284
- x: (seed.x - 0.5) * shape.size.x,
285
- y: (seed.y - 0.5) * shape.size.y,
286
- z: (seed.z - 0.5) * shape.size.z,
287
- };
288
- }
289
- else if (shape.emitFrom === 'surface') {
290
- const face = Math.floor(seed.x * 6);
291
- const u = seed.y - 0.5;
292
- const v = seed.z - 0.5;
293
- switch (face) {
294
- case 0:
295
- position = { x: shape.size.x / 2, y: u * shape.size.y, z: v * shape.size.z };
296
- break;
297
- case 1:
298
- position = { x: -shape.size.x / 2, y: u * shape.size.y, z: v * shape.size.z };
299
- break;
300
- case 2:
301
- position = { x: u * shape.size.x, y: shape.size.y / 2, z: v * shape.size.z };
302
- break;
303
- case 3:
304
- position = { x: u * shape.size.x, y: -shape.size.y / 2, z: v * shape.size.z };
305
- break;
306
- case 4:
307
- position = { x: u * shape.size.x, y: v * shape.size.y, z: shape.size.z / 2 };
308
- break;
309
- default:
310
- position = { x: u * shape.size.x, y: v * shape.size.y, z: -shape.size.z / 2 };
311
- }
312
- }
313
- direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
281
+ case 3:
282
+ position = { x: u * shape.size.x, y: -shape.size.y / 2, z: v * shape.size.z };
314
283
  break;
315
- }
316
- case 'cone': {
317
- const radius = evaluateDistribution(shape.radius, 0, seed.x);
318
- const arc = (shape.arc * Math.PI) / 180;
319
- const theta = seed.z * arc;
320
- const angle = (shape.angle * Math.PI) / 180;
321
- const r = radius * seed.y;
322
- const h = seed.x * shape.length;
323
- const coneRadius = r + h * Math.tan(angle);
324
- position = {
325
- x: coneRadius * Math.cos(theta),
326
- y: h,
327
- z: coneRadius * Math.sin(theta),
328
- };
329
- direction = vec3Normalize({
330
- x: Math.cos(theta) * Math.sin(angle),
331
- y: Math.cos(angle),
332
- z: Math.sin(theta) * Math.sin(angle),
333
- });
334
- break;
335
- }
336
- case 'circle': {
337
- const radius = evaluateDistribution(shape.radius, 0, seed.x);
338
- const innerRadius = radius * (1 - shape.radiusThickness);
339
- const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
340
- const arc = (shape.arc * Math.PI) / 180;
341
- const theta = seed.z * arc;
342
- position = {
343
- x: r * Math.cos(theta),
344
- y: 0,
345
- z: r * Math.sin(theta),
346
- };
347
- direction = shape.randomDirection
348
- ? vec3Normalize({ x: Math.random() - 0.5, y: 1, z: Math.random() - 0.5 })
349
- : { x: 0, y: 1, z: 0 };
284
+ case 4:
285
+ position = { x: u * shape.size.x, y: v * shape.size.y, z: shape.size.z / 2 };
350
286
  break;
287
+ default:
288
+ position = { x: u * shape.size.x, y: v * shape.size.y, z: -shape.size.z / 2 };
351
289
  }
352
- case 'edge':
353
- position = {
354
- x: (seed.x - 0.5) * shape.length,
355
- y: 0,
356
- z: 0,
357
- };
358
- direction = { x: 0, y: 1, z: 0 };
359
- break;
360
- case 'rectangle':
361
- position = {
362
- x: (seed.x - 0.5) * shape.size.x,
363
- y: 0,
364
- z: (seed.z - 0.5) * shape.size.y,
365
- };
366
- direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
367
- break;
290
+ }
291
+ direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
292
+ break;
293
+ }
294
+ case "cone": {
295
+ const radius = evaluateDistribution(shape.radius, 0, seed.x);
296
+ const arc = shape.arc * Math.PI / 180;
297
+ const theta = seed.z * arc;
298
+ const angle = shape.angle * Math.PI / 180;
299
+ const r = radius * seed.y;
300
+ const h = seed.x * shape.length;
301
+ const coneRadius = r + h * Math.tan(angle);
302
+ position = {
303
+ x: coneRadius * Math.cos(theta),
304
+ y: h,
305
+ z: coneRadius * Math.sin(theta)
306
+ };
307
+ direction = vec3Normalize({
308
+ x: Math.cos(theta) * Math.sin(angle),
309
+ y: Math.cos(angle),
310
+ z: Math.sin(theta) * Math.sin(angle)
311
+ });
312
+ break;
313
+ }
314
+ case "circle": {
315
+ const radius = evaluateDistribution(shape.radius, 0, seed.x);
316
+ const innerRadius = radius * (1 - shape.radiusThickness);
317
+ const r = shape.emitFromShell ? radius : innerRadius + seed.y * (radius - innerRadius);
318
+ const arc = shape.arc * Math.PI / 180;
319
+ const theta = seed.z * arc;
320
+ position = {
321
+ x: r * Math.cos(theta),
322
+ y: 0,
323
+ z: r * Math.sin(theta)
324
+ };
325
+ direction = shape.randomDirection ? vec3Normalize({ x: Math.random() - 0.5, y: 1, z: Math.random() - 0.5 }) : { x: 0, y: 1, z: 0 };
326
+ break;
368
327
  }
369
- return { position, direction };
328
+ case "edge":
329
+ position = {
330
+ x: (seed.x - 0.5) * shape.length,
331
+ y: 0,
332
+ z: 0
333
+ };
334
+ direction = { x: 0, y: 1, z: 0 };
335
+ break;
336
+ case "rectangle":
337
+ position = {
338
+ x: (seed.x - 0.5) * shape.size.x,
339
+ y: 0,
340
+ z: (seed.z - 0.5) * shape.size.y
341
+ };
342
+ direction = shape.randomDirection ? vec3RandomOnSphere() : { x: 0, y: 1, z: 0 };
343
+ break;
344
+ }
345
+ return { position, direction };
370
346
  }
371
- // ─── Force Application ───────────────────────────────────────────────────────
372
347
  function applyForce(particle, force, dt, time) {
373
- if (!force.enabled) {
348
+ if (!force.enabled) {
349
+ return { x: 0, y: 0, z: 0 };
350
+ }
351
+ const t = particle.age / particle.startLifetime;
352
+ const strength = evaluateDistribution(force.strength, t, particle.randomSeed);
353
+ switch (force.type) {
354
+ case "gravity":
355
+ return vec3Mul(force.direction, strength * dt);
356
+ case "wind": {
357
+ const turbulenceOffset = perlinNoise2D(
358
+ particle.position.x * force.frequency + time,
359
+ particle.position.z * force.frequency
360
+ ) * force.turbulence;
361
+ const windDir = vec3Add(force.direction, { x: turbulenceOffset, y: 0, z: turbulenceOffset });
362
+ return vec3Mul(vec3Normalize(windDir), strength * dt);
363
+ }
364
+ case "vortex": {
365
+ const toCenter = vec3Sub(force.position, particle.position);
366
+ const dist = vec3Length(toCenter);
367
+ if (dist < 1e-3 || dist > force.size) {
374
368
  return { x: 0, y: 0, z: 0 };
369
+ }
370
+ const tangent = vec3Normalize(vec3Cross(force.axis, toCenter));
371
+ const speed = evaluateDistribution(force.speed, t, particle.randomSeed);
372
+ const pull = evaluateDistribution(force.pullStrength, t, particle.randomSeed);
373
+ const tangentForce = vec3Mul(tangent, speed * strength * dt);
374
+ const pullForce = vec3Mul(vec3Normalize(toCenter), pull * strength * dt);
375
+ return vec3Add(tangentForce, pullForce);
375
376
  }
376
- const t = particle.age / particle.startLifetime;
377
- const strength = evaluateDistribution(force.strength, t, particle.randomSeed);
378
- switch (force.type) {
379
- case 'gravity':
380
- return vec3Mul(force.direction, strength * dt);
381
- case 'wind': {
382
- const turbulenceOffset = perlinNoise2D(particle.position.x * force.frequency + time, particle.position.z * force.frequency) * force.turbulence;
383
- const windDir = vec3Add(force.direction, { x: turbulenceOffset, y: 0, z: turbulenceOffset });
384
- return vec3Mul(vec3Normalize(windDir), strength * dt);
385
- }
386
- case 'vortex': {
387
- const toCenter = vec3Sub(force.position, particle.position);
388
- const dist = vec3Length(toCenter);
389
- if (dist < 0.001 || dist > force.size) {
390
- return { x: 0, y: 0, z: 0 };
391
- }
392
- const tangent = vec3Normalize(vec3Cross(force.axis, toCenter));
393
- const speed = evaluateDistribution(force.speed, t, particle.randomSeed);
394
- const pull = evaluateDistribution(force.pullStrength, t, particle.randomSeed);
395
- const tangentForce = vec3Mul(tangent, speed * strength * dt);
396
- const pullForce = vec3Mul(vec3Normalize(toCenter), pull * strength * dt);
397
- return vec3Add(tangentForce, pullForce);
398
- }
399
- case 'turbulence': {
400
- const freqX = force.frequency.x;
401
- const freqY = force.frequency.y;
402
- const freqZ = force.frequency.z;
403
- const scrollX = time * force.scrollSpeed.x;
404
- const scrollY = time * force.scrollSpeed.y;
405
- const scrollZ = time * force.scrollSpeed.z;
406
- const noiseX = fbmNoise(particle.position.x * freqX + scrollX, particle.position.y * freqY, particle.position.z * freqZ, force.octaves, force.roughness) *
407
- 2 -
408
- 1;
409
- const noiseY = fbmNoise(particle.position.x * freqX, particle.position.y * freqY + scrollY + 100, particle.position.z * freqZ, force.octaves, force.roughness) *
410
- 2 -
411
- 1;
412
- const noiseZ = fbmNoise(particle.position.x * freqX, particle.position.y * freqY, particle.position.z * freqZ + scrollZ + 200, force.octaves, force.roughness) *
413
- 2 -
414
- 1;
415
- return {
416
- x: noiseX * evaluateDistribution(force.strengthX, t, particle.randomSeed) * dt,
417
- y: noiseY * evaluateDistribution(force.strengthY, t, particle.randomSeed) * dt,
418
- z: noiseZ * evaluateDistribution(force.strengthZ, t, particle.randomSeed) * dt,
419
- };
420
- }
421
- case 'attractor': {
422
- const toAttractor = vec3Sub(force.position, particle.position);
423
- const dist = vec3Length(toAttractor);
424
- if (dist < 0.001) {
425
- if (force.killParticles) {
426
- particle.age = particle.startLifetime; // Kill particle
427
- }
428
- return { x: 0, y: 0, z: 0 };
429
- }
430
- if (dist > force.radius) {
431
- return { x: 0, y: 0, z: 0 };
432
- }
433
- let falloff = 1;
434
- switch (force.falloff) {
435
- case 'linear':
436
- falloff = 1 - dist / force.radius;
437
- break;
438
- case 'quadratic':
439
- falloff = Math.pow(1 - dist / force.radius, 2);
440
- break;
441
- }
442
- return vec3Mul(vec3Normalize(toAttractor), strength * falloff * dt);
443
- }
444
- case 'drag': {
445
- const drag = evaluateDistribution(force.linearDrag, t, particle.randomSeed);
446
- let multiplier = 1;
447
- if (force.multiplyBySize) {
448
- const size = (particle.size.x + particle.size.y + particle.size.z) / 3;
449
- multiplier *= size;
450
- }
451
- if (force.multiplyByVelocity) {
452
- multiplier *= vec3Length(particle.velocity);
453
- }
454
- return vec3Mul(particle.velocity, -drag * multiplier * dt);
377
+ case "turbulence": {
378
+ const freqX = force.frequency.x;
379
+ const freqY = force.frequency.y;
380
+ const freqZ = force.frequency.z;
381
+ const scrollX = time * force.scrollSpeed.x;
382
+ const scrollY = time * force.scrollSpeed.y;
383
+ const scrollZ = time * force.scrollSpeed.z;
384
+ const noiseX = fbmNoise(
385
+ particle.position.x * freqX + scrollX,
386
+ particle.position.y * freqY,
387
+ particle.position.z * freqZ,
388
+ force.octaves,
389
+ force.roughness
390
+ ) * 2 - 1;
391
+ const noiseY = fbmNoise(
392
+ particle.position.x * freqX,
393
+ particle.position.y * freqY + scrollY + 100,
394
+ particle.position.z * freqZ,
395
+ force.octaves,
396
+ force.roughness
397
+ ) * 2 - 1;
398
+ const noiseZ = fbmNoise(
399
+ particle.position.x * freqX,
400
+ particle.position.y * freqY,
401
+ particle.position.z * freqZ + scrollZ + 200,
402
+ force.octaves,
403
+ force.roughness
404
+ ) * 2 - 1;
405
+ return {
406
+ x: noiseX * evaluateDistribution(force.strengthX, t, particle.randomSeed) * dt,
407
+ y: noiseY * evaluateDistribution(force.strengthY, t, particle.randomSeed) * dt,
408
+ z: noiseZ * evaluateDistribution(force.strengthZ, t, particle.randomSeed) * dt
409
+ };
410
+ }
411
+ case "attractor": {
412
+ const toAttractor = vec3Sub(force.position, particle.position);
413
+ const dist = vec3Length(toAttractor);
414
+ if (dist < 1e-3) {
415
+ if (force.killParticles) {
416
+ particle.age = particle.startLifetime;
455
417
  }
456
- default:
457
- return { x: 0, y: 0, z: 0 };
418
+ return { x: 0, y: 0, z: 0 };
419
+ }
420
+ if (dist > force.radius) {
421
+ return { x: 0, y: 0, z: 0 };
422
+ }
423
+ let falloff = 1;
424
+ switch (force.falloff) {
425
+ case "linear":
426
+ falloff = 1 - dist / force.radius;
427
+ break;
428
+ case "quadratic":
429
+ falloff = Math.pow(1 - dist / force.radius, 2);
430
+ break;
431
+ }
432
+ return vec3Mul(vec3Normalize(toAttractor), strength * falloff * dt);
433
+ }
434
+ case "drag": {
435
+ const drag = evaluateDistribution(force.linearDrag, t, particle.randomSeed);
436
+ let multiplier = 1;
437
+ if (force.multiplyBySize) {
438
+ const size = (particle.size.x + particle.size.y + particle.size.z) / 3;
439
+ multiplier *= size;
440
+ }
441
+ if (force.multiplyByVelocity) {
442
+ multiplier *= vec3Length(particle.velocity);
443
+ }
444
+ return vec3Mul(particle.velocity, -drag * multiplier * dt);
458
445
  }
446
+ default:
447
+ return { x: 0, y: 0, z: 0 };
448
+ }
459
449
  }
460
- // ─── System Creation ─────────────────────────────────────────────────────────
461
- function createDefaultSystem(name = 'New Particle System') {
462
- return {
463
- id: generateSystemId(),
464
- name,
465
- version: 1,
466
- // Main
467
- duration: 5,
468
- looping: true,
469
- prewarm: false,
470
- startDelay: { type: 'constant', value: 0 },
471
- startLifetime: { type: 'random', min: 3, max: 5 },
472
- startSpeed: { type: 'constant', value: 5 },
473
- startSize3D: false,
474
- startSize: { type: 'random', min: 0.5, max: 1 },
475
- startRotation3D: false,
476
- startRotation: { type: 'constant', value: 0 },
477
- flipRotation: 0,
478
- startColor: {
479
- colorKeys: [{ time: 0, color: { r: 1, g: 1, b: 1 } }],
480
- alphaKeys: [{ time: 0, alpha: 1 }],
481
- },
482
- gravityModifier: { type: 'constant', value: 0 },
483
- simulationSpace: 'local',
484
- simulationSpeed: 1,
485
- deltaTimeType: 'scaled',
486
- scalingMode: 'hierarchy',
487
- playOnAwake: true,
488
- // Emission
489
- emission: {
490
- enabled: true,
491
- rateOverTime: { type: 'constant', value: 10 },
492
- rateOverDistance: { type: 'constant', value: 0 },
493
- bursts: [],
494
- },
495
- // Shape
496
- shape: {
497
- type: 'cone',
498
- emitFromShell: false,
499
- randomDirection: false,
500
- sphericalDirection: false,
501
- arcMode: 'random',
502
- arcSpread: 0,
503
- radius: { type: 'constant', value: 1 },
504
- angle: 25,
505
- length: 5,
506
- radiusThickness: 1,
507
- arc: 360,
508
- emitFrom: 'base',
509
- },
510
- // Velocity over lifetime
511
- velocityOverLifetime: {
512
- enabled: false,
513
- linear: {
514
- x: { type: 'constant', value: 0 },
515
- y: { type: 'constant', value: 0 },
516
- z: { type: 'constant', value: 0 },
517
- },
518
- space: 'local',
519
- orbital: {
520
- x: { type: 'constant', value: 0 },
521
- y: { type: 'constant', value: 0 },
522
- z: { type: 'constant', value: 0 },
523
- },
524
- offset: {
525
- x: { type: 'constant', value: 0 },
526
- y: { type: 'constant', value: 0 },
527
- z: { type: 'constant', value: 0 },
528
- },
529
- radial: { type: 'constant', value: 0 },
530
- speedModifier: { type: 'constant', value: 1 },
531
- },
532
- // Limit velocity
533
- limitVelocityOverLifetime: {
534
- enabled: false,
535
- separateAxes: false,
536
- speed: { type: 'constant', value: 10 },
537
- dampen: 0.1,
538
- drag: { type: 'constant', value: 0 },
539
- multiplyBySize: false,
540
- space: 'local',
541
- },
542
- // Inherit velocity
543
- inheritVelocity: {
544
- enabled: false,
545
- mode: 'initial',
546
- curve: { type: 'constant', value: 0 },
547
- },
548
- // Lifetime by emitter speed
549
- lifetimeByEmitterSpeed: {
550
- enabled: false,
551
- curve: DEFAULT_CURVE,
552
- speedRange: { min: 0, max: 1 },
553
- },
554
- // Force over lifetime
555
- forceOverLifetime: {
556
- enabled: false,
557
- force: {
558
- x: { type: 'constant', value: 0 },
559
- y: { type: 'constant', value: 0 },
560
- z: { type: 'constant', value: 0 },
561
- },
562
- space: 'local',
563
- randomized: false,
564
- },
565
- // Color over lifetime
566
- colorOverLifetime: {
567
- enabled: true,
568
- color: {
569
- colorKeys: [
570
- { time: 0, color: { r: 1, g: 1, b: 1 } },
571
- { time: 1, color: { r: 1, g: 1, b: 1 } },
572
- ],
573
- alphaKeys: [
574
- { time: 0, alpha: 1 },
575
- { time: 0.9, alpha: 1 },
576
- { time: 1, alpha: 0 },
577
- ],
578
- },
579
- },
580
- // Color by speed
581
- colorBySpeed: {
582
- enabled: false,
583
- color: DEFAULT_GRADIENT,
584
- range: { min: 0, max: 1 },
585
- },
586
- // Size over lifetime
587
- sizeOverLifetime: {
588
- enabled: false,
589
- separateAxes: false,
590
- size: {
591
- type: 'curve',
592
- curve: [
593
- { time: 0, value: 1 },
594
- { time: 1, value: 0 },
595
- ],
596
- },
597
- },
598
- // Size by speed
599
- sizeBySpeed: {
600
- enabled: false,
601
- separateAxes: false,
602
- size: { type: 'constant', value: 1 },
603
- range: { min: 0, max: 1 },
604
- },
605
- // Rotation over lifetime
606
- rotationOverLifetime: {
607
- enabled: false,
608
- separateAxes: false,
609
- angularVelocity: { type: 'constant', value: 0 },
610
- },
611
- // Rotation by speed
612
- rotationBySpeed: {
613
- enabled: false,
614
- separateAxes: false,
615
- angularVelocity: { type: 'constant', value: 0 },
616
- range: { min: 0, max: 1 },
617
- },
618
- // External forces
619
- externalForces: [],
620
- // Noise
621
- noise: {
622
- enabled: false,
623
- strength: { type: 'constant', value: 1 },
624
- frequency: 0.5,
625
- scrollSpeed: 0,
626
- damping: true,
627
- octaves: 1,
628
- octaveMultiplier: 0.5,
629
- octaveScale: 2,
630
- positionAmount: {
631
- x: { type: 'constant', value: 1 },
632
- y: { type: 'constant', value: 1 },
633
- z: { type: 'constant', value: 1 },
634
- },
635
- rotationAmount: {
636
- x: { type: 'constant', value: 0 },
637
- y: { type: 'constant', value: 0 },
638
- z: { type: 'constant', value: 0 },
639
- },
640
- sizeAmount: { type: 'constant', value: 0 },
641
- separateAxes: false,
642
- remap: DEFAULT_CURVE,
643
- remapEnabled: false,
644
- },
645
- // Collision
646
- collision: {
647
- enabled: false,
648
- type: 'world',
649
- mode: '3d',
650
- dampen: { type: 'constant', value: 0 },
651
- bounce: { type: 'constant', value: 1 },
652
- lifetimeLoss: { type: 'constant', value: 0 },
653
- minKillSpeed: 0,
654
- maxKillSpeed: 10000,
655
- radiusScale: 1,
656
- sendCollisionMessages: false,
657
- maxCollisionShapes: 256,
658
- quality: 'medium',
659
- },
660
- // Triggers
661
- triggers: {
662
- enabled: false,
663
- inside: 'ignore',
664
- outside: 'ignore',
665
- enter: 'ignore',
666
- exit: 'ignore',
667
- radiusScale: 1,
668
- colliderQueryMode: 'disabled',
669
- },
670
- // Sub-emitters
671
- subEmitters: [],
672
- // Texture sheet animation
673
- textureSheetAnimation: {
674
- enabled: false,
675
- mode: 'grid',
676
- tilesX: 1,
677
- tilesY: 1,
678
- animation: 'whole_sheet',
679
- timeMode: 'lifetime',
680
- cycleCount: 1,
681
- },
682
- // Lights
683
- lights: {
684
- enabled: false,
685
- ratio: 1,
686
- randomDistribution: false,
687
- useParticleColor: true,
688
- sizeAffectsRange: false,
689
- alphaAffectsIntensity: false,
690
- range: { type: 'constant', value: 1 },
691
- intensity: { type: 'constant', value: 1 },
692
- maxLights: 20,
693
- },
694
- // Trails
695
- trails: null,
696
- // Renderer
697
- renderer: {
698
- type: 'billboard',
699
- sortMode: 'none',
700
- renderAlignment: 'view',
701
- },
702
- // Material
703
- material: {
704
- shading: 'unlit',
705
- blendMode: 'additive',
706
- colorMode: 'multiply',
707
- flipBookBlending: false,
708
- shadowCasting: 'off',
709
- shadowReceiving: false,
710
- cullMode: 'off',
711
- sortingFudge: 0,
712
- minParticleSize: 0,
713
- maxParticleSize: 0.5,
714
- pivot: { x: 0, y: 0, z: 0 },
715
- customVertexStreams: [],
716
- },
717
- maxParticles: 1000,
718
- gpuInstancing: false,
719
- };
450
+ function createDefaultSystem(name = "New Particle System") {
451
+ return {
452
+ id: generateSystemId(),
453
+ name,
454
+ version: 1,
455
+ // Main
456
+ duration: 5,
457
+ looping: true,
458
+ prewarm: false,
459
+ startDelay: { type: "constant", value: 0 },
460
+ startLifetime: { type: "random", min: 3, max: 5 },
461
+ startSpeed: { type: "constant", value: 5 },
462
+ startSize3D: false,
463
+ startSize: { type: "random", min: 0.5, max: 1 },
464
+ startRotation3D: false,
465
+ startRotation: { type: "constant", value: 0 },
466
+ flipRotation: 0,
467
+ startColor: {
468
+ colorKeys: [{ time: 0, color: { r: 1, g: 1, b: 1 } }],
469
+ alphaKeys: [{ time: 0, alpha: 1 }]
470
+ },
471
+ gravityModifier: { type: "constant", value: 0 },
472
+ simulationSpace: "local",
473
+ simulationSpeed: 1,
474
+ deltaTimeType: "scaled",
475
+ scalingMode: "hierarchy",
476
+ playOnAwake: true,
477
+ // Emission
478
+ emission: {
479
+ enabled: true,
480
+ rateOverTime: { type: "constant", value: 10 },
481
+ rateOverDistance: { type: "constant", value: 0 },
482
+ bursts: []
483
+ },
484
+ // Shape
485
+ shape: {
486
+ type: "cone",
487
+ emitFromShell: false,
488
+ randomDirection: false,
489
+ sphericalDirection: false,
490
+ arcMode: "random",
491
+ arcSpread: 0,
492
+ radius: { type: "constant", value: 1 },
493
+ angle: 25,
494
+ length: 5,
495
+ radiusThickness: 1,
496
+ arc: 360,
497
+ emitFrom: "base"
498
+ },
499
+ // Velocity over lifetime
500
+ velocityOverLifetime: {
501
+ enabled: false,
502
+ linear: {
503
+ x: { type: "constant", value: 0 },
504
+ y: { type: "constant", value: 0 },
505
+ z: { type: "constant", value: 0 }
506
+ },
507
+ space: "local",
508
+ orbital: {
509
+ x: { type: "constant", value: 0 },
510
+ y: { type: "constant", value: 0 },
511
+ z: { type: "constant", value: 0 }
512
+ },
513
+ offset: {
514
+ x: { type: "constant", value: 0 },
515
+ y: { type: "constant", value: 0 },
516
+ z: { type: "constant", value: 0 }
517
+ },
518
+ radial: { type: "constant", value: 0 },
519
+ speedModifier: { type: "constant", value: 1 }
520
+ },
521
+ // Limit velocity
522
+ limitVelocityOverLifetime: {
523
+ enabled: false,
524
+ separateAxes: false,
525
+ speed: { type: "constant", value: 10 },
526
+ dampen: 0.1,
527
+ drag: { type: "constant", value: 0 },
528
+ multiplyBySize: false,
529
+ space: "local"
530
+ },
531
+ // Inherit velocity
532
+ inheritVelocity: {
533
+ enabled: false,
534
+ mode: "initial",
535
+ curve: { type: "constant", value: 0 }
536
+ },
537
+ // Lifetime by emitter speed
538
+ lifetimeByEmitterSpeed: {
539
+ enabled: false,
540
+ curve: DEFAULT_CURVE,
541
+ speedRange: { min: 0, max: 1 }
542
+ },
543
+ // Force over lifetime
544
+ forceOverLifetime: {
545
+ enabled: false,
546
+ force: {
547
+ x: { type: "constant", value: 0 },
548
+ y: { type: "constant", value: 0 },
549
+ z: { type: "constant", value: 0 }
550
+ },
551
+ space: "local",
552
+ randomized: false
553
+ },
554
+ // Color over lifetime
555
+ colorOverLifetime: {
556
+ enabled: true,
557
+ color: {
558
+ colorKeys: [
559
+ { time: 0, color: { r: 1, g: 1, b: 1 } },
560
+ { time: 1, color: { r: 1, g: 1, b: 1 } }
561
+ ],
562
+ alphaKeys: [
563
+ { time: 0, alpha: 1 },
564
+ { time: 0.9, alpha: 1 },
565
+ { time: 1, alpha: 0 }
566
+ ]
567
+ }
568
+ },
569
+ // Color by speed
570
+ colorBySpeed: {
571
+ enabled: false,
572
+ color: DEFAULT_GRADIENT,
573
+ range: { min: 0, max: 1 }
574
+ },
575
+ // Size over lifetime
576
+ sizeOverLifetime: {
577
+ enabled: false,
578
+ separateAxes: false,
579
+ size: {
580
+ type: "curve",
581
+ curve: [
582
+ { time: 0, value: 1 },
583
+ { time: 1, value: 0 }
584
+ ]
585
+ }
586
+ },
587
+ // Size by speed
588
+ sizeBySpeed: {
589
+ enabled: false,
590
+ separateAxes: false,
591
+ size: { type: "constant", value: 1 },
592
+ range: { min: 0, max: 1 }
593
+ },
594
+ // Rotation over lifetime
595
+ rotationOverLifetime: {
596
+ enabled: false,
597
+ separateAxes: false,
598
+ angularVelocity: { type: "constant", value: 0 }
599
+ },
600
+ // Rotation by speed
601
+ rotationBySpeed: {
602
+ enabled: false,
603
+ separateAxes: false,
604
+ angularVelocity: { type: "constant", value: 0 },
605
+ range: { min: 0, max: 1 }
606
+ },
607
+ // External forces
608
+ externalForces: [],
609
+ // Noise
610
+ noise: {
611
+ enabled: false,
612
+ strength: { type: "constant", value: 1 },
613
+ frequency: 0.5,
614
+ scrollSpeed: 0,
615
+ damping: true,
616
+ octaves: 1,
617
+ octaveMultiplier: 0.5,
618
+ octaveScale: 2,
619
+ positionAmount: {
620
+ x: { type: "constant", value: 1 },
621
+ y: { type: "constant", value: 1 },
622
+ z: { type: "constant", value: 1 }
623
+ },
624
+ rotationAmount: {
625
+ x: { type: "constant", value: 0 },
626
+ y: { type: "constant", value: 0 },
627
+ z: { type: "constant", value: 0 }
628
+ },
629
+ sizeAmount: { type: "constant", value: 0 },
630
+ separateAxes: false,
631
+ remap: DEFAULT_CURVE,
632
+ remapEnabled: false
633
+ },
634
+ // Collision
635
+ collision: {
636
+ enabled: false,
637
+ type: "world",
638
+ mode: "3d",
639
+ dampen: { type: "constant", value: 0 },
640
+ bounce: { type: "constant", value: 1 },
641
+ lifetimeLoss: { type: "constant", value: 0 },
642
+ minKillSpeed: 0,
643
+ maxKillSpeed: 1e4,
644
+ radiusScale: 1,
645
+ sendCollisionMessages: false,
646
+ maxCollisionShapes: 256,
647
+ quality: "medium"
648
+ },
649
+ // Triggers
650
+ triggers: {
651
+ enabled: false,
652
+ inside: "ignore",
653
+ outside: "ignore",
654
+ enter: "ignore",
655
+ exit: "ignore",
656
+ radiusScale: 1,
657
+ colliderQueryMode: "disabled"
658
+ },
659
+ // Sub-emitters
660
+ subEmitters: [],
661
+ // Texture sheet animation
662
+ textureSheetAnimation: {
663
+ enabled: false,
664
+ mode: "grid",
665
+ tilesX: 1,
666
+ tilesY: 1,
667
+ animation: "whole_sheet",
668
+ timeMode: "lifetime",
669
+ cycleCount: 1
670
+ },
671
+ // Lights
672
+ lights: {
673
+ enabled: false,
674
+ ratio: 1,
675
+ randomDistribution: false,
676
+ useParticleColor: true,
677
+ sizeAffectsRange: false,
678
+ alphaAffectsIntensity: false,
679
+ range: { type: "constant", value: 1 },
680
+ intensity: { type: "constant", value: 1 },
681
+ maxLights: 20
682
+ },
683
+ // Trails
684
+ trails: null,
685
+ // Renderer
686
+ renderer: {
687
+ type: "billboard",
688
+ sortMode: "none",
689
+ renderAlignment: "view"
690
+ },
691
+ // Material
692
+ material: {
693
+ shading: "unlit",
694
+ blendMode: "additive",
695
+ colorMode: "multiply",
696
+ flipBookBlending: false,
697
+ shadowCasting: "off",
698
+ shadowReceiving: false,
699
+ cullMode: "off",
700
+ sortingFudge: 0,
701
+ minParticleSize: 0,
702
+ maxParticleSize: 0.5,
703
+ pivot: { x: 0, y: 0, z: 0 },
704
+ customVertexStreams: []
705
+ },
706
+ maxParticles: 1e3,
707
+ gpuInstancing: false
708
+ };
720
709
  }
721
- // ─── Presets ─────────────────────────────────────────────────────────────────
722
710
  const PARTICLE_PRESETS = {
723
- fire: () => ({
724
- name: 'Fire',
725
- duration: 1,
726
- startLifetime: { type: 'random', min: 0.5, max: 1.5 },
727
- startSpeed: { type: 'random', min: 1, max: 3 },
728
- startSize: { type: 'random', min: 0.3, max: 0.8 },
729
- gravityModifier: { type: 'constant', value: -0.5 },
730
- emission: {
731
- enabled: true,
732
- rateOverTime: { type: 'constant', value: 50 },
733
- rateOverDistance: { type: 'constant', value: 0 },
734
- bursts: [],
735
- },
736
- colorOverLifetime: {
737
- enabled: true,
738
- color: {
739
- colorKeys: [
740
- { time: 0, color: { r: 1, g: 0.8, b: 0.2 } },
741
- { time: 0.5, color: { r: 1, g: 0.4, b: 0 } },
742
- { time: 1, color: { r: 0.3, g: 0, b: 0 } },
743
- ],
744
- alphaKeys: [
745
- { time: 0, alpha: 0.8 },
746
- { time: 1, alpha: 0 },
747
- ],
748
- },
749
- },
750
- sizeOverLifetime: {
751
- enabled: true,
752
- separateAxes: false,
753
- size: {
754
- type: 'curve',
755
- curve: [
756
- { time: 0, value: 0.5 },
757
- { time: 0.5, value: 1 },
758
- { time: 1, value: 0.2 },
759
- ],
760
- },
761
- },
762
- material: {
763
- shading: 'unlit',
764
- blendMode: 'additive',
765
- colorMode: 'multiply',
766
- flipBookBlending: false,
767
- shadowCasting: 'off',
768
- shadowReceiving: false,
769
- cullMode: 'off',
770
- sortingFudge: 0,
771
- minParticleSize: 0,
772
- maxParticleSize: 0.5,
773
- pivot: { x: 0, y: 0, z: 0 },
774
- customVertexStreams: [],
775
- },
776
- }),
777
- smoke: () => ({
778
- name: 'Smoke',
779
- duration: 3,
780
- startLifetime: { type: 'random', min: 3, max: 5 },
781
- startSpeed: { type: 'random', min: 0.5, max: 1.5 },
782
- startSize: { type: 'random', min: 0.5, max: 1.5 },
783
- gravityModifier: { type: 'constant', value: -0.1 },
784
- emission: {
785
- enabled: true,
786
- rateOverTime: { type: 'constant', value: 10 },
787
- rateOverDistance: { type: 'constant', value: 0 },
788
- bursts: [],
789
- },
790
- colorOverLifetime: {
791
- enabled: true,
792
- color: {
793
- colorKeys: [
794
- { time: 0, color: { r: 0.5, g: 0.5, b: 0.5 } },
795
- { time: 1, color: { r: 0.3, g: 0.3, b: 0.3 } },
796
- ],
797
- alphaKeys: [
798
- { time: 0, alpha: 0.4 },
799
- { time: 0.7, alpha: 0.2 },
800
- { time: 1, alpha: 0 },
801
- ],
802
- },
803
- },
804
- sizeOverLifetime: {
805
- enabled: true,
806
- separateAxes: false,
807
- size: {
808
- type: 'curve',
809
- curve: [
810
- { time: 0, value: 0.5 },
811
- { time: 1, value: 2 },
812
- ],
813
- },
814
- },
815
- noise: {
816
- enabled: true,
817
- strength: { type: 'constant', value: 0.5 },
818
- frequency: 0.5,
819
- scrollSpeed: 0.2,
820
- damping: true,
821
- octaves: 2,
822
- octaveMultiplier: 0.5,
823
- octaveScale: 2,
824
- positionAmount: {
825
- x: { type: 'constant', value: 1 },
826
- y: { type: 'constant', value: 0.5 },
827
- z: { type: 'constant', value: 1 },
828
- },
829
- rotationAmount: {
830
- x: { type: 'constant', value: 0 },
831
- y: { type: 'constant', value: 0 },
832
- z: { type: 'constant', value: 0 },
833
- },
834
- sizeAmount: { type: 'constant', value: 0 },
835
- separateAxes: false,
836
- remap: DEFAULT_CURVE,
837
- remapEnabled: false,
838
- },
839
- material: {
840
- shading: 'unlit',
841
- blendMode: 'alpha',
842
- colorMode: 'multiply',
843
- flipBookBlending: false,
844
- shadowCasting: 'off',
845
- shadowReceiving: false,
846
- cullMode: 'off',
847
- sortingFudge: 0,
848
- minParticleSize: 0,
849
- maxParticleSize: 2,
850
- pivot: { x: 0, y: 0, z: 0 },
851
- customVertexStreams: [],
852
- },
853
- }),
854
- sparks: () => ({
855
- name: 'Sparks',
856
- duration: 0.5,
857
- looping: false,
858
- startLifetime: { type: 'random', min: 0.3, max: 0.8 },
859
- startSpeed: { type: 'random', min: 5, max: 15 },
860
- startSize: { type: 'random', min: 0.02, max: 0.05 },
861
- gravityModifier: { type: 'constant', value: 1 },
862
- emission: {
863
- enabled: true,
864
- rateOverTime: { type: 'constant', value: 0 },
865
- rateOverDistance: { type: 'constant', value: 0 },
866
- bursts: [
867
- {
868
- time: 0,
869
- count: { type: 'random', min: 20, max: 40 },
870
- cycles: 1,
871
- interval: 0.1,
872
- probability: 1,
873
- },
874
- ],
875
- },
876
- shape: {
877
- type: 'sphere',
878
- emitFromShell: true,
879
- randomDirection: true,
880
- sphericalDirection: true,
881
- arcMode: 'random',
882
- arcSpread: 0,
883
- radius: { type: 'constant', value: 0.1 },
884
- radiusThickness: 0,
885
- arc: 360,
886
- },
887
- colorOverLifetime: {
888
- enabled: true,
889
- color: {
890
- colorKeys: [
891
- { time: 0, color: { r: 1, g: 0.9, b: 0.4 } },
892
- { time: 0.5, color: { r: 1, g: 0.5, b: 0.1 } },
893
- { time: 1, color: { r: 0.8, g: 0.2, b: 0 } },
894
- ],
895
- alphaKeys: [
896
- { time: 0, alpha: 1 },
897
- { time: 0.8, alpha: 1 },
898
- { time: 1, alpha: 0 },
899
- ],
900
- },
901
- },
902
- renderer: {
903
- type: 'stretched_billboard',
904
- sortMode: 'none',
905
- renderAlignment: 'velocity',
906
- speedScale: 0.02,
907
- lengthScale: 1,
908
- faceCamera: true,
909
- },
910
- material: {
911
- shading: 'unlit',
912
- blendMode: 'additive',
913
- colorMode: 'multiply',
914
- flipBookBlending: false,
915
- shadowCasting: 'off',
916
- shadowReceiving: false,
917
- cullMode: 'off',
918
- sortingFudge: 0,
919
- minParticleSize: 0,
920
- maxParticleSize: 0.1,
921
- pivot: { x: 0, y: 0, z: 0 },
922
- customVertexStreams: [],
923
- },
924
- }),
925
- snow: () => ({
926
- name: 'Snow',
927
- duration: 10,
928
- startLifetime: { type: 'random', min: 10, max: 15 },
929
- startSpeed: { type: 'random', min: 0.5, max: 1 },
930
- startSize: { type: 'random', min: 0.05, max: 0.15 },
931
- gravityModifier: { type: 'constant', value: 0.1 },
932
- shape: {
933
- type: 'box',
934
- emitFromShell: false,
935
- randomDirection: false,
936
- sphericalDirection: false,
937
- arcMode: 'random',
938
- arcSpread: 0,
939
- size: { x: 20, y: 0.1, z: 20 },
940
- emitFrom: 'volume',
941
- },
942
- emission: {
943
- enabled: true,
944
- rateOverTime: { type: 'constant', value: 100 },
945
- rateOverDistance: { type: 'constant', value: 0 },
946
- bursts: [],
947
- },
948
- colorOverLifetime: {
949
- enabled: true,
950
- color: {
951
- colorKeys: [{ time: 0, color: { r: 1, g: 1, b: 1 } }],
952
- alphaKeys: [
953
- { time: 0, alpha: 0 },
954
- { time: 0.1, alpha: 0.8 },
955
- { time: 0.9, alpha: 0.8 },
956
- { time: 1, alpha: 0 },
957
- ],
958
- },
959
- },
960
- noise: {
961
- enabled: true,
962
- strength: { type: 'constant', value: 0.3 },
963
- frequency: 0.3,
964
- scrollSpeed: 0.1,
965
- damping: false,
966
- octaves: 2,
967
- octaveMultiplier: 0.5,
968
- octaveScale: 2,
969
- positionAmount: {
970
- x: { type: 'constant', value: 1 },
971
- y: { type: 'constant', value: 0.2 },
972
- z: { type: 'constant', value: 1 },
973
- },
974
- rotationAmount: {
975
- x: { type: 'constant', value: 0 },
976
- y: { type: 'constant', value: 0 },
977
- z: { type: 'constant', value: 0 },
978
- },
979
- sizeAmount: { type: 'constant', value: 0 },
980
- separateAxes: false,
981
- remap: DEFAULT_CURVE,
982
- remapEnabled: false,
983
- },
984
- material: {
985
- shading: 'unlit',
986
- blendMode: 'alpha',
987
- colorMode: 'multiply',
988
- flipBookBlending: false,
989
- shadowCasting: 'off',
990
- shadowReceiving: false,
991
- cullMode: 'off',
992
- sortingFudge: 0,
993
- minParticleSize: 0,
994
- maxParticleSize: 0.2,
995
- pivot: { x: 0, y: 0, z: 0 },
996
- customVertexStreams: [],
997
- },
998
- }),
999
- rain: () => ({
1000
- name: 'Rain',
1001
- duration: 5,
1002
- startLifetime: { type: 'constant', value: 1 },
1003
- startSpeed: { type: 'random', min: 15, max: 20 },
1004
- startSize: { type: 'constant', value: 0.03 },
1005
- startRotation: { type: 'constant', value: 0 },
1006
- gravityModifier: { type: 'constant', value: 0 },
1007
- shape: {
1008
- type: 'box',
1009
- emitFromShell: false,
1010
- randomDirection: false,
1011
- sphericalDirection: false,
1012
- arcMode: 'random',
1013
- arcSpread: 0,
1014
- size: { x: 20, y: 0.1, z: 20 },
1015
- emitFrom: 'volume',
1016
- },
1017
- emission: {
1018
- enabled: true,
1019
- rateOverTime: { type: 'constant', value: 500 },
1020
- rateOverDistance: { type: 'constant', value: 0 },
1021
- bursts: [],
1022
- },
1023
- colorOverLifetime: {
1024
- enabled: true,
1025
- color: {
1026
- colorKeys: [{ time: 0, color: { r: 0.7, g: 0.8, b: 0.9 } }],
1027
- alphaKeys: [
1028
- { time: 0, alpha: 0 },
1029
- { time: 0.1, alpha: 0.5 },
1030
- { time: 0.9, alpha: 0.5 },
1031
- { time: 1, alpha: 0 },
1032
- ],
1033
- },
1034
- },
1035
- renderer: {
1036
- type: 'stretched_billboard',
1037
- sortMode: 'none',
1038
- renderAlignment: 'velocity',
1039
- speedScale: 0.01,
1040
- lengthScale: 3,
1041
- faceCamera: true,
1042
- },
1043
- material: {
1044
- shading: 'unlit',
1045
- blendMode: 'alpha',
1046
- colorMode: 'multiply',
1047
- flipBookBlending: false,
1048
- shadowCasting: 'off',
1049
- shadowReceiving: false,
1050
- cullMode: 'off',
1051
- sortingFudge: 0,
1052
- minParticleSize: 0,
1053
- maxParticleSize: 0.1,
1054
- pivot: { x: 0, y: 0, z: 0 },
1055
- customVertexStreams: [],
1056
- },
1057
- }),
711
+ fire: () => ({
712
+ name: "Fire",
713
+ duration: 1,
714
+ startLifetime: { type: "random", min: 0.5, max: 1.5 },
715
+ startSpeed: { type: "random", min: 1, max: 3 },
716
+ startSize: { type: "random", min: 0.3, max: 0.8 },
717
+ gravityModifier: { type: "constant", value: -0.5 },
718
+ emission: {
719
+ enabled: true,
720
+ rateOverTime: { type: "constant", value: 50 },
721
+ rateOverDistance: { type: "constant", value: 0 },
722
+ bursts: []
723
+ },
724
+ colorOverLifetime: {
725
+ enabled: true,
726
+ color: {
727
+ colorKeys: [
728
+ { time: 0, color: { r: 1, g: 0.8, b: 0.2 } },
729
+ { time: 0.5, color: { r: 1, g: 0.4, b: 0 } },
730
+ { time: 1, color: { r: 0.3, g: 0, b: 0 } }
731
+ ],
732
+ alphaKeys: [
733
+ { time: 0, alpha: 0.8 },
734
+ { time: 1, alpha: 0 }
735
+ ]
736
+ }
737
+ },
738
+ sizeOverLifetime: {
739
+ enabled: true,
740
+ separateAxes: false,
741
+ size: {
742
+ type: "curve",
743
+ curve: [
744
+ { time: 0, value: 0.5 },
745
+ { time: 0.5, value: 1 },
746
+ { time: 1, value: 0.2 }
747
+ ]
748
+ }
749
+ },
750
+ material: {
751
+ shading: "unlit",
752
+ blendMode: "additive",
753
+ colorMode: "multiply",
754
+ flipBookBlending: false,
755
+ shadowCasting: "off",
756
+ shadowReceiving: false,
757
+ cullMode: "off",
758
+ sortingFudge: 0,
759
+ minParticleSize: 0,
760
+ maxParticleSize: 0.5,
761
+ pivot: { x: 0, y: 0, z: 0 },
762
+ customVertexStreams: []
763
+ }
764
+ }),
765
+ smoke: () => ({
766
+ name: "Smoke",
767
+ duration: 3,
768
+ startLifetime: { type: "random", min: 3, max: 5 },
769
+ startSpeed: { type: "random", min: 0.5, max: 1.5 },
770
+ startSize: { type: "random", min: 0.5, max: 1.5 },
771
+ gravityModifier: { type: "constant", value: -0.1 },
772
+ emission: {
773
+ enabled: true,
774
+ rateOverTime: { type: "constant", value: 10 },
775
+ rateOverDistance: { type: "constant", value: 0 },
776
+ bursts: []
777
+ },
778
+ colorOverLifetime: {
779
+ enabled: true,
780
+ color: {
781
+ colorKeys: [
782
+ { time: 0, color: { r: 0.5, g: 0.5, b: 0.5 } },
783
+ { time: 1, color: { r: 0.3, g: 0.3, b: 0.3 } }
784
+ ],
785
+ alphaKeys: [
786
+ { time: 0, alpha: 0.4 },
787
+ { time: 0.7, alpha: 0.2 },
788
+ { time: 1, alpha: 0 }
789
+ ]
790
+ }
791
+ },
792
+ sizeOverLifetime: {
793
+ enabled: true,
794
+ separateAxes: false,
795
+ size: {
796
+ type: "curve",
797
+ curve: [
798
+ { time: 0, value: 0.5 },
799
+ { time: 1, value: 2 }
800
+ ]
801
+ }
802
+ },
803
+ noise: {
804
+ enabled: true,
805
+ strength: { type: "constant", value: 0.5 },
806
+ frequency: 0.5,
807
+ scrollSpeed: 0.2,
808
+ damping: true,
809
+ octaves: 2,
810
+ octaveMultiplier: 0.5,
811
+ octaveScale: 2,
812
+ positionAmount: {
813
+ x: { type: "constant", value: 1 },
814
+ y: { type: "constant", value: 0.5 },
815
+ z: { type: "constant", value: 1 }
816
+ },
817
+ rotationAmount: {
818
+ x: { type: "constant", value: 0 },
819
+ y: { type: "constant", value: 0 },
820
+ z: { type: "constant", value: 0 }
821
+ },
822
+ sizeAmount: { type: "constant", value: 0 },
823
+ separateAxes: false,
824
+ remap: DEFAULT_CURVE,
825
+ remapEnabled: false
826
+ },
827
+ material: {
828
+ shading: "unlit",
829
+ blendMode: "alpha",
830
+ colorMode: "multiply",
831
+ flipBookBlending: false,
832
+ shadowCasting: "off",
833
+ shadowReceiving: false,
834
+ cullMode: "off",
835
+ sortingFudge: 0,
836
+ minParticleSize: 0,
837
+ maxParticleSize: 2,
838
+ pivot: { x: 0, y: 0, z: 0 },
839
+ customVertexStreams: []
840
+ }
841
+ }),
842
+ sparks: () => ({
843
+ name: "Sparks",
844
+ duration: 0.5,
845
+ looping: false,
846
+ startLifetime: { type: "random", min: 0.3, max: 0.8 },
847
+ startSpeed: { type: "random", min: 5, max: 15 },
848
+ startSize: { type: "random", min: 0.02, max: 0.05 },
849
+ gravityModifier: { type: "constant", value: 1 },
850
+ emission: {
851
+ enabled: true,
852
+ rateOverTime: { type: "constant", value: 0 },
853
+ rateOverDistance: { type: "constant", value: 0 },
854
+ bursts: [
855
+ {
856
+ time: 0,
857
+ count: { type: "random", min: 20, max: 40 },
858
+ cycles: 1,
859
+ interval: 0.1,
860
+ probability: 1
861
+ }
862
+ ]
863
+ },
864
+ shape: {
865
+ type: "sphere",
866
+ emitFromShell: true,
867
+ randomDirection: true,
868
+ sphericalDirection: true,
869
+ arcMode: "random",
870
+ arcSpread: 0,
871
+ radius: { type: "constant", value: 0.1 },
872
+ radiusThickness: 0,
873
+ arc: 360
874
+ },
875
+ colorOverLifetime: {
876
+ enabled: true,
877
+ color: {
878
+ colorKeys: [
879
+ { time: 0, color: { r: 1, g: 0.9, b: 0.4 } },
880
+ { time: 0.5, color: { r: 1, g: 0.5, b: 0.1 } },
881
+ { time: 1, color: { r: 0.8, g: 0.2, b: 0 } }
882
+ ],
883
+ alphaKeys: [
884
+ { time: 0, alpha: 1 },
885
+ { time: 0.8, alpha: 1 },
886
+ { time: 1, alpha: 0 }
887
+ ]
888
+ }
889
+ },
890
+ renderer: {
891
+ type: "stretched_billboard",
892
+ sortMode: "none",
893
+ renderAlignment: "velocity",
894
+ speedScale: 0.02,
895
+ lengthScale: 1,
896
+ faceCamera: true
897
+ },
898
+ material: {
899
+ shading: "unlit",
900
+ blendMode: "additive",
901
+ colorMode: "multiply",
902
+ flipBookBlending: false,
903
+ shadowCasting: "off",
904
+ shadowReceiving: false,
905
+ cullMode: "off",
906
+ sortingFudge: 0,
907
+ minParticleSize: 0,
908
+ maxParticleSize: 0.1,
909
+ pivot: { x: 0, y: 0, z: 0 },
910
+ customVertexStreams: []
911
+ }
912
+ }),
913
+ snow: () => ({
914
+ name: "Snow",
915
+ duration: 10,
916
+ startLifetime: { type: "random", min: 10, max: 15 },
917
+ startSpeed: { type: "random", min: 0.5, max: 1 },
918
+ startSize: { type: "random", min: 0.05, max: 0.15 },
919
+ gravityModifier: { type: "constant", value: 0.1 },
920
+ shape: {
921
+ type: "box",
922
+ emitFromShell: false,
923
+ randomDirection: false,
924
+ sphericalDirection: false,
925
+ arcMode: "random",
926
+ arcSpread: 0,
927
+ size: { x: 20, y: 0.1, z: 20 },
928
+ emitFrom: "volume"
929
+ },
930
+ emission: {
931
+ enabled: true,
932
+ rateOverTime: { type: "constant", value: 100 },
933
+ rateOverDistance: { type: "constant", value: 0 },
934
+ bursts: []
935
+ },
936
+ colorOverLifetime: {
937
+ enabled: true,
938
+ color: {
939
+ colorKeys: [{ time: 0, color: { r: 1, g: 1, b: 1 } }],
940
+ alphaKeys: [
941
+ { time: 0, alpha: 0 },
942
+ { time: 0.1, alpha: 0.8 },
943
+ { time: 0.9, alpha: 0.8 },
944
+ { time: 1, alpha: 0 }
945
+ ]
946
+ }
947
+ },
948
+ noise: {
949
+ enabled: true,
950
+ strength: { type: "constant", value: 0.3 },
951
+ frequency: 0.3,
952
+ scrollSpeed: 0.1,
953
+ damping: false,
954
+ octaves: 2,
955
+ octaveMultiplier: 0.5,
956
+ octaveScale: 2,
957
+ positionAmount: {
958
+ x: { type: "constant", value: 1 },
959
+ y: { type: "constant", value: 0.2 },
960
+ z: { type: "constant", value: 1 }
961
+ },
962
+ rotationAmount: {
963
+ x: { type: "constant", value: 0 },
964
+ y: { type: "constant", value: 0 },
965
+ z: { type: "constant", value: 0 }
966
+ },
967
+ sizeAmount: { type: "constant", value: 0 },
968
+ separateAxes: false,
969
+ remap: DEFAULT_CURVE,
970
+ remapEnabled: false
971
+ },
972
+ material: {
973
+ shading: "unlit",
974
+ blendMode: "alpha",
975
+ colorMode: "multiply",
976
+ flipBookBlending: false,
977
+ shadowCasting: "off",
978
+ shadowReceiving: false,
979
+ cullMode: "off",
980
+ sortingFudge: 0,
981
+ minParticleSize: 0,
982
+ maxParticleSize: 0.2,
983
+ pivot: { x: 0, y: 0, z: 0 },
984
+ customVertexStreams: []
985
+ }
986
+ }),
987
+ rain: () => ({
988
+ name: "Rain",
989
+ duration: 5,
990
+ startLifetime: { type: "constant", value: 1 },
991
+ startSpeed: { type: "random", min: 15, max: 20 },
992
+ startSize: { type: "constant", value: 0.03 },
993
+ startRotation: { type: "constant", value: 0 },
994
+ gravityModifier: { type: "constant", value: 0 },
995
+ shape: {
996
+ type: "box",
997
+ emitFromShell: false,
998
+ randomDirection: false,
999
+ sphericalDirection: false,
1000
+ arcMode: "random",
1001
+ arcSpread: 0,
1002
+ size: { x: 20, y: 0.1, z: 20 },
1003
+ emitFrom: "volume"
1004
+ },
1005
+ emission: {
1006
+ enabled: true,
1007
+ rateOverTime: { type: "constant", value: 500 },
1008
+ rateOverDistance: { type: "constant", value: 0 },
1009
+ bursts: []
1010
+ },
1011
+ colorOverLifetime: {
1012
+ enabled: true,
1013
+ color: {
1014
+ colorKeys: [{ time: 0, color: { r: 0.7, g: 0.8, b: 0.9 } }],
1015
+ alphaKeys: [
1016
+ { time: 0, alpha: 0 },
1017
+ { time: 0.1, alpha: 0.5 },
1018
+ { time: 0.9, alpha: 0.5 },
1019
+ { time: 1, alpha: 0 }
1020
+ ]
1021
+ }
1022
+ },
1023
+ renderer: {
1024
+ type: "stretched_billboard",
1025
+ sortMode: "none",
1026
+ renderAlignment: "velocity",
1027
+ speedScale: 0.01,
1028
+ lengthScale: 3,
1029
+ faceCamera: true
1030
+ },
1031
+ material: {
1032
+ shading: "unlit",
1033
+ blendMode: "alpha",
1034
+ colorMode: "multiply",
1035
+ flipBookBlending: false,
1036
+ shadowCasting: "off",
1037
+ shadowReceiving: false,
1038
+ cullMode: "off",
1039
+ sortingFudge: 0,
1040
+ minParticleSize: 0,
1041
+ maxParticleSize: 0.1,
1042
+ pivot: { x: 0, y: 0, z: 0 },
1043
+ customVertexStreams: []
1044
+ }
1045
+ })
1058
1046
  };
1059
1047
  function applyPreset(preset) {
1060
- const base = createDefaultSystem();
1061
- const presetData = PARTICLE_PRESETS[preset]();
1062
- return { ...base, ...presetData };
1048
+ const base = createDefaultSystem();
1049
+ const presetData = PARTICLE_PRESETS[preset]();
1050
+ return { ...base, ...presetData };
1063
1051
  }
1064
- // ─── Import/Export ───────────────────────────────────────────────────────────
1065
1052
  function exportSystemsToJSON(systems) {
1066
- return JSON.stringify(systems, null, 2);
1053
+ return JSON.stringify(systems, null, 2);
1067
1054
  }
1068
1055
  function importSystemsFromJSON(json) {
1069
- try {
1070
- const data = JSON.parse(json);
1071
- if (Array.isArray(data)) {
1072
- return data;
1073
- }
1074
- if (data && typeof data === 'object' && data.id) {
1075
- return [data];
1076
- }
1077
- return null;
1056
+ try {
1057
+ const data = JSON.parse(json);
1058
+ if (Array.isArray(data)) {
1059
+ return data;
1078
1060
  }
1079
- catch (_a) {
1080
- return null;
1061
+ if (data && typeof data === "object" && data.id) {
1062
+ return [data];
1081
1063
  }
1064
+ return null;
1065
+ } catch {
1066
+ return null;
1067
+ }
1082
1068
  }
1083
1069
  function cloneSystem(system) {
1084
- const cloned = JSON.parse(JSON.stringify(system));
1085
- cloned.id = generateSystemId();
1086
- cloned.name = `${system.name} (Copy)`;
1087
- return cloned;
1070
+ const cloned = JSON.parse(JSON.stringify(system));
1071
+ cloned.id = generateSystemId();
1072
+ cloned.name = `${system.name} (Copy)`;
1073
+ return cloned;
1088
1074
  }
1089
-
1090
- export { PARTICLE_PRESETS, applyForce, applyPreset, cloneSystem, createDefaultSystem, evaluateCurve, evaluateDistribution, evaluateGradient, evaluateVec3Distribution, exportSystemsToJSON, fbmNoise, generateForceId, generateSubEmitterId, generateSystemId, importSystemsFromJSON, perlinNoise2D, sampleEmitterShape, seededRandom, vec3Add, vec3Cross, vec3Dot, vec3Length, vec3Lerp, vec3Mul, vec3MulVec, vec3Normalize, vec3Random, vec3RandomOnSphere, vec3Sub };
1075
+ export {
1076
+ PARTICLE_PRESETS,
1077
+ applyForce,
1078
+ applyPreset,
1079
+ cloneSystem,
1080
+ createDefaultSystem,
1081
+ evaluateCurve,
1082
+ evaluateDistribution,
1083
+ evaluateGradient,
1084
+ evaluateVec3Distribution,
1085
+ exportSystemsToJSON,
1086
+ fbmNoise,
1087
+ generateForceId,
1088
+ generateSubEmitterId,
1089
+ generateSystemId,
1090
+ importSystemsFromJSON,
1091
+ perlinNoise2D,
1092
+ sampleEmitterShape,
1093
+ seededRandom,
1094
+ vec3Add,
1095
+ vec3Cross,
1096
+ vec3Dot,
1097
+ vec3Length,
1098
+ vec3Lerp,
1099
+ vec3Mul,
1100
+ vec3MulVec,
1101
+ vec3Normalize,
1102
+ vec3Random,
1103
+ vec3RandomOnSphere,
1104
+ vec3Sub
1105
+ };