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