@codexo/exojs 0.7.13 → 0.8.0

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 (134) hide show
  1. package/CHANGELOG.md +403 -0
  2. package/dist/esm/index.js +28 -7
  3. package/dist/esm/index.js.map +1 -1
  4. package/dist/esm/particles/ParticleSystem.d.ts +180 -83
  5. package/dist/esm/particles/ParticleSystem.js +446 -133
  6. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  7. package/dist/esm/particles/distributions/BoxArea.d.ts +17 -0
  8. package/dist/esm/particles/distributions/BoxArea.js +48 -0
  9. package/dist/esm/particles/distributions/BoxArea.js.map +1 -0
  10. package/dist/esm/particles/distributions/CircleArea.d.ts +19 -0
  11. package/dist/esm/particles/distributions/CircleArea.js +33 -0
  12. package/dist/esm/particles/distributions/CircleArea.js.map +1 -0
  13. package/dist/esm/particles/distributions/ConeDirection.d.ts +28 -0
  14. package/dist/esm/particles/distributions/ConeDirection.js +44 -0
  15. package/dist/esm/particles/distributions/ConeDirection.js.map +1 -0
  16. package/dist/esm/particles/distributions/Constant.d.ts +17 -0
  17. package/dist/esm/particles/distributions/Constant.js +35 -0
  18. package/dist/esm/particles/distributions/Constant.js.map +1 -0
  19. package/dist/esm/particles/distributions/Curve.d.ts +30 -0
  20. package/dist/esm/particles/distributions/Curve.js +53 -0
  21. package/dist/esm/particles/distributions/Curve.js.map +1 -0
  22. package/dist/esm/particles/distributions/Distribution.d.ts +45 -0
  23. package/dist/esm/particles/distributions/Gradient.d.ts +40 -0
  24. package/dist/esm/particles/distributions/Gradient.js +72 -0
  25. package/dist/esm/particles/distributions/Gradient.js.map +1 -0
  26. package/dist/esm/particles/distributions/LineSegment.d.ts +15 -0
  27. package/dist/esm/particles/distributions/LineSegment.js +27 -0
  28. package/dist/esm/particles/distributions/LineSegment.js.map +1 -0
  29. package/dist/esm/particles/distributions/Range.d.ts +12 -0
  30. package/dist/esm/particles/distributions/Range.js +19 -0
  31. package/dist/esm/particles/distributions/Range.js.map +1 -0
  32. package/dist/esm/particles/distributions/VectorRange.d.ts +20 -0
  33. package/dist/esm/particles/distributions/VectorRange.js +31 -0
  34. package/dist/esm/particles/distributions/VectorRange.js.map +1 -0
  35. package/dist/esm/particles/distributions/index.d.ts +12 -0
  36. package/dist/esm/particles/gpu/ParticleGpuState.d.ts +57 -0
  37. package/dist/esm/particles/gpu/ParticleGpuState.js +508 -0
  38. package/dist/esm/particles/gpu/ParticleGpuState.js.map +1 -0
  39. package/dist/esm/particles/index.d.ts +2 -10
  40. package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +24 -0
  41. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +60 -0
  42. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -0
  43. package/dist/esm/particles/modules/ApplyForce.d.ts +20 -0
  44. package/dist/esm/particles/modules/ApplyForce.js +48 -0
  45. package/dist/esm/particles/modules/ApplyForce.js.map +1 -0
  46. package/dist/esm/particles/modules/AttractToPoint.d.ts +27 -0
  47. package/dist/esm/particles/modules/AttractToPoint.js +73 -0
  48. package/dist/esm/particles/modules/AttractToPoint.js.map +1 -0
  49. package/dist/esm/particles/modules/BurstSpawn.d.ts +53 -0
  50. package/dist/esm/particles/modules/BurstSpawn.js +94 -0
  51. package/dist/esm/particles/modules/BurstSpawn.js.map +1 -0
  52. package/dist/esm/particles/modules/ColorOverLifetime.d.ts +22 -0
  53. package/dist/esm/particles/modules/ColorOverLifetime.js +65 -0
  54. package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -0
  55. package/dist/esm/particles/modules/ColorOverSpeed.d.ts +27 -0
  56. package/dist/esm/particles/modules/ColorOverSpeed.js +86 -0
  57. package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -0
  58. package/dist/esm/particles/modules/DeathModule.d.ts +24 -0
  59. package/dist/esm/particles/modules/DeathModule.js +25 -0
  60. package/dist/esm/particles/modules/DeathModule.js.map +1 -0
  61. package/dist/esm/particles/modules/Drag.d.ts +20 -0
  62. package/dist/esm/particles/modules/Drag.js +45 -0
  63. package/dist/esm/particles/modules/Drag.js.map +1 -0
  64. package/dist/esm/particles/modules/OrbitalForce.d.ts +28 -0
  65. package/dist/esm/particles/modules/OrbitalForce.js +65 -0
  66. package/dist/esm/particles/modules/OrbitalForce.js.map +1 -0
  67. package/dist/esm/particles/modules/RateSpawn.d.ts +41 -0
  68. package/dist/esm/particles/modules/RateSpawn.js +76 -0
  69. package/dist/esm/particles/modules/RateSpawn.js.map +1 -0
  70. package/dist/esm/particles/modules/RepelFromPoint.d.ts +24 -0
  71. package/dist/esm/particles/modules/RepelFromPoint.js +76 -0
  72. package/dist/esm/particles/modules/RepelFromPoint.js.map +1 -0
  73. package/dist/esm/particles/modules/RotateOverLifetime.d.ts +20 -0
  74. package/dist/esm/particles/modules/RotateOverLifetime.js +43 -0
  75. package/dist/esm/particles/modules/RotateOverLifetime.js.map +1 -0
  76. package/dist/esm/particles/modules/ScaleOverLifetime.d.ts +26 -0
  77. package/dist/esm/particles/modules/ScaleOverLifetime.js +59 -0
  78. package/dist/esm/particles/modules/ScaleOverLifetime.js.map +1 -0
  79. package/dist/esm/particles/modules/SpawnModule.d.ts +30 -0
  80. package/dist/esm/particles/modules/SpawnModule.js +31 -0
  81. package/dist/esm/particles/modules/SpawnModule.js.map +1 -0
  82. package/dist/esm/particles/modules/SpawnOnDeath.d.ts +24 -0
  83. package/dist/esm/particles/modules/SpawnOnDeath.js +47 -0
  84. package/dist/esm/particles/modules/SpawnOnDeath.js.map +1 -0
  85. package/dist/esm/particles/modules/Turbulence.d.ts +30 -0
  86. package/dist/esm/particles/modules/Turbulence.js +122 -0
  87. package/dist/esm/particles/modules/Turbulence.js.map +1 -0
  88. package/dist/esm/particles/modules/UpdateModule.d.ts +95 -0
  89. package/dist/esm/particles/modules/UpdateModule.js +66 -0
  90. package/dist/esm/particles/modules/UpdateModule.js.map +1 -0
  91. package/dist/esm/particles/modules/VelocityOverLifetime.d.ts +30 -0
  92. package/dist/esm/particles/modules/VelocityOverLifetime.js +84 -0
  93. package/dist/esm/particles/modules/VelocityOverLifetime.js.map +1 -0
  94. package/dist/esm/particles/modules/WgslContribution.d.ts +81 -0
  95. package/dist/esm/particles/modules/WgslContribution.js +34 -0
  96. package/dist/esm/particles/modules/WgslContribution.js.map +1 -0
  97. package/dist/esm/particles/modules/index.d.ts +22 -0
  98. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +9 -14
  99. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +90 -61
  100. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
  101. package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
  102. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +9 -0
  103. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +107 -23
  104. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  105. package/dist/esm/rendering/webgpu/compute/WebGpuComputePipeline.d.ts +52 -0
  106. package/dist/esm/rendering/webgpu/compute/WebGpuStorageBuffer.d.ts +29 -0
  107. package/dist/esm/rendering/webgpu/compute/index.d.ts +3 -0
  108. package/dist/exo.esm.js +2657 -742
  109. package/dist/exo.esm.js.map +1 -1
  110. package/package.json +1 -1
  111. package/dist/esm/particles/Particle.d.ts +0 -77
  112. package/dist/esm/particles/Particle.js +0 -143
  113. package/dist/esm/particles/Particle.js.map +0 -1
  114. package/dist/esm/particles/ParticleProperties.d.ts +0 -29
  115. package/dist/esm/particles/affectors/ColorAffector.d.ts +0 -30
  116. package/dist/esm/particles/affectors/ColorAffector.js +0 -55
  117. package/dist/esm/particles/affectors/ColorAffector.js.map +0 -1
  118. package/dist/esm/particles/affectors/ForceAffector.d.ts +0 -24
  119. package/dist/esm/particles/affectors/ForceAffector.js +0 -39
  120. package/dist/esm/particles/affectors/ForceAffector.js.map +0 -1
  121. package/dist/esm/particles/affectors/ParticleAffector.d.ts +0 -19
  122. package/dist/esm/particles/affectors/ScaleAffector.d.ts +0 -23
  123. package/dist/esm/particles/affectors/ScaleAffector.js +0 -38
  124. package/dist/esm/particles/affectors/ScaleAffector.js.map +0 -1
  125. package/dist/esm/particles/affectors/TorqueAffector.d.ts +0 -23
  126. package/dist/esm/particles/affectors/TorqueAffector.js +0 -37
  127. package/dist/esm/particles/affectors/TorqueAffector.js.map +0 -1
  128. package/dist/esm/particles/emitters/ParticleEmitter.d.ts +0 -19
  129. package/dist/esm/particles/emitters/ParticleOptions.d.ts +0 -62
  130. package/dist/esm/particles/emitters/ParticleOptions.js +0 -120
  131. package/dist/esm/particles/emitters/ParticleOptions.js.map +0 -1
  132. package/dist/esm/particles/emitters/UniversalEmitter.d.ts +0 -40
  133. package/dist/esm/particles/emitters/UniversalEmitter.js +0 -68
  134. package/dist/esm/particles/emitters/UniversalEmitter.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,409 @@ All notable changes to ExoJS are documented in this file.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.8.0] - 2026-05-07
8
+
9
+ Wholesale rewrite of the particle subsystem around a data-oriented core
10
+ plus a backend-agnostic auto-routing pipeline. The `Particle` class,
11
+ `ParticleAffector` interface, `ParticleEmitter` interface,
12
+ `ParticleOptions`, `UniversalEmitter`, and the four built-in affectors
13
+ (`ColorAffector`, `ForceAffector`, `ScaleAffector`, `TorqueAffector`)
14
+ are removed. They are replaced by SoA storage on the system,
15
+ `Distribution<T>`-based spawn configs, and per-batch
16
+ `SpawnModule` / `UpdateModule` / `DeathModule` interfaces.
17
+
18
+ Update modules now declare an optional `wgsl()` contribution — when
19
+ the system is constructed with a `WebGpuBackend` and every registered
20
+ update module is GPU-eligible (i.e. all built-ins, plus any custom
21
+ modules the author opts in), a composite WGSL compute shader is built
22
+ at first `update()`. Integration + every module body + pack-instances
23
+ all run in **one dispatch**, writing directly into the renderer's
24
+ instance vertex buffer. **No CPU readback** in the steady state.
25
+
26
+ On WebGL2 backends, or when any registered update module lacks
27
+ `wgsl()`, the system runs the existing CPU pipeline. The decision is
28
+ automatic and per-system; user code is unchanged across both paths.
29
+
30
+ ### Added — Struct-of-Arrays storage
31
+
32
+ `ParticleSystem` now stores particles as parallel `Float32Array` /
33
+ `Uint32Array` / `Uint16Array` channels addressed by slot index:
34
+
35
+ ```ts
36
+ system.posX[slot]
37
+ system.posY[slot]
38
+ system.velX[slot]
39
+ system.velY[slot]
40
+ system.scaleX[slot]
41
+ system.scaleY[slot]
42
+ system.rotations[slot]
43
+ system.rotationSpeeds[slot]
44
+ system.color[slot] // packed 0xAABBGGRR
45
+ system.elapsed[slot]
46
+ system.lifetime[slot]
47
+ system.textureIndex[slot]
48
+ system.liveCount // [0, liveCount) is the live range
49
+ ```
50
+
51
+ Capacity is fixed at construction (default 4096) — no reallocations.
52
+ The integrate pass runs as one tight loop over typed arrays with no
53
+ method calls. Expiry is handled by forward-compaction (O(n) total
54
+ instead of the previous O(n²) splice loop with scattered expirations).
55
+
56
+ ### Added — `Distribution<T>` family
57
+
58
+ Spawn-time random sampling and lifetime-parameterised evaluation:
59
+
60
+ | Type | Use |
61
+ |---|---|
62
+ | `Constant<T>` | Always-same value |
63
+ | `Range` | Uniform random number in `[min, max]` |
64
+ | `VectorRange` | Per-axis random vector |
65
+ | `ConeDirection` | Random unit vector in a cone × speed range |
66
+ | `CircleArea` | Random point in/on a circle |
67
+ | `BoxArea` | Random point in/on an AABB |
68
+ | `LineSegment` | Random point on a segment |
69
+ | `Curve` | Piecewise-linear keyframe scalar by lifetime ratio |
70
+ | `Gradient` | Piecewise-linear keyframe color, with `evaluateRgba()` for direct u32 packing |
71
+
72
+ `Curve` and `Gradient` cache the last segment so monotonically
73
+ advancing `t` (the typical case for per-particle lifetime sampling)
74
+ is O(1) amortised.
75
+
76
+ ### Added — Module pipeline
77
+
78
+ Three module bases. Each registered on a system via the corresponding
79
+ `addX` method; each runs in its declared phase per-frame.
80
+
81
+ ```ts
82
+ abstract class SpawnModule { apply(system, dt: number): void; }
83
+ abstract class UpdateModule { apply(system, dt: number): void; }
84
+ abstract class DeathModule { onDeath(system, slot: number): void; }
85
+ ```
86
+
87
+ **Built-in spawn modules:**
88
+
89
+ - `RateSpawn({ rate, lifetime?, position?, velocity?, scale?, rotation?, rotationSpeed?, tint?, textureIndex? })`
90
+ — continuous emission with sub-frame accumulator. Each property is an
91
+ independent `Distribution<T>`.
92
+ - `BurstSpawn({ schedule, loop?, ...samePropsAsRate })` — discrete
93
+ bursts at scheduled times. Use for explosions, level-ups,
94
+ hit-impacts.
95
+
96
+ **Built-in update modules** (operate on the SoA arrays in tight loops):
97
+
98
+ - `ApplyForce(ax, ay)` — adds constant acceleration.
99
+ - `Drag(coefficient)` — exponential velocity damping.
100
+ - `ColorOverLifetime(gradient)` — tint sampled from a `Gradient`.
101
+ - `ScaleOverLifetime(curve)` — both axes sampled from a `Curve`.
102
+ - `RotateOverLifetime(angularAccel)` — increments `rotationSpeed`.
103
+
104
+ **Built-in death module:**
105
+
106
+ - `SpawnOnDeath(targetSystem, spawner, count?)` — sub-emitter. Forwards
107
+ the dying particle's position to a target system's spawn module.
108
+ Use for explosion-on-impact, end-of-life sparks, multi-stage VFX.
109
+
110
+ ### Added — Backend-agnostic auto-routing GPU compute pipeline
111
+
112
+ New `src/rendering/webgpu/compute/` infrastructure:
113
+
114
+ - `WebGpuStorageBuffer` — owning wrapper over a `STORAGE | COPY_DST | COPY_SRC`
115
+ buffer with `write()` and async `read()` helpers.
116
+ - `WebGpuComputePipeline` — `device.createComputePipeline` wrapper with
117
+ bind-group-layout creation, dispatch helper.
118
+
119
+ New `src/particles/gpu/ParticleGpuState` — owns the GPU-side mirror
120
+ for one `ParticleSystem`. At construction time it:
121
+
122
+ 1. Walks the registered update modules, collecting each module's
123
+ `WgslContribution` (uniform field declarations + texture bindings
124
+ + WGSL body snippet).
125
+ 2. Generates a composite WGSL compute shader: SoA storage bindings +
126
+ sim/module uniform structs + module texture bindings + a `main`
127
+ function containing integration → all module bodies in registration
128
+ order → pack-instances writing interleaved 24-byte instances into
129
+ a `STORAGE | VERTEX` buffer.
130
+ 3. Allocates 7 packed storage buffers (positions/velocities/scales/
131
+ rotInfo/timing as `vec2<f32>` arrays plus color as `u32` plus the
132
+ instance output) — fits within WebGPU's default
133
+ `maxStorageBuffersPerShaderStage = 8` limit.
134
+ 4. Allocates 1D textures for any module that declares them
135
+ (`Curve` → 256-tap r32float; `Gradient` → 256-tap rgba8unorm) and
136
+ uploads the lookup data once via `module.uploadTextures()`.
137
+ 5. Each module's `writeUniforms()` runs every frame to update its
138
+ slice of the shared module-uniform buffer.
139
+
140
+ The `WebGpuParticleRenderer` reads the GPU-written instance buffer
141
+ directly when `system.gpuMode` is true; CPU mode falls back to the
142
+ existing CPU-pack path. Same renderer, same vertex layout, no copy
143
+ between simulation and render.
144
+
145
+ `UpdateModule` gains optional `wgsl()`, `writeUniforms()`,
146
+ `uploadTextures()`. Built-in modules ship all three. Custom modules
147
+ that implement them get GPU acceleration; modules with only `apply()`
148
+ keep working but force their host system into CPU mode.
149
+
150
+ Opt-in is a single constructor option — no imperative toggle:
151
+
152
+ ```ts
153
+ const system = new ParticleSystem(texture, {
154
+ capacity: 8192,
155
+ backend: app.backend, // CPU-routed on WebGL2, GPU-routed on WebGPU
156
+ });
157
+ ```
158
+
159
+ The `backend` reference is duck-typed against `WebGpuBackend`; on
160
+ WebGL2 it's recorded but never used. The system's mode is locked in
161
+ at the first `update()` (when modules are introspected); adding update
162
+ modules after that throws.
163
+
164
+ ### Removed — Old particle API (BREAKING)
165
+
166
+ The following symbols are deleted. Migration recipes follow the table.
167
+
168
+ | Removed | Replacement |
169
+ |---|---|
170
+ | `Particle` (class) | SoA arrays on `ParticleSystem` (`system.posX[slot]`, etc.) |
171
+ | `ParticleProperties` (interface) | None — slot-indexed arrays replace the per-particle object |
172
+ | `ParticleEmitter` (interface) | `SpawnModule` (abstract class) |
173
+ | `ParticleOptions` | Per-property `Distribution<T>` in the spawn module's config |
174
+ | `UniversalEmitter` | `RateSpawn` |
175
+ | `ParticleAffector` (interface) | `UpdateModule` (abstract class) |
176
+ | `ColorAffector` | `ColorOverLifetime` + `Gradient` |
177
+ | `ForceAffector` | `ApplyForce` |
178
+ | `ScaleAffector` | `ScaleOverLifetime` + `Curve` |
179
+ | `TorqueAffector` | `RotateOverLifetime` |
180
+ | `system.requestParticle()` | `system.spawn(): number` (slot index, or `-1` at capacity) |
181
+ | `system.emitParticle(p)` | (gone — `spawn()` already commits the slot to the live range) |
182
+ | `system.updateParticle(p, dt)` | (gone — internal to `update()`) |
183
+ | `system.addEmitter(e)` | `system.addSpawnModule(m)` |
184
+ | `system.addAffector(a)` | `system.addUpdateModule(m)` |
185
+ | `system.particles` (`Array<Particle>`) | `system.posX` / `system.posY` / ... `system.liveCount` |
186
+ | `system.graveyard` | (gone — no graveyard; slots are recycled in place) |
187
+
188
+ ### Migration
189
+
190
+ ```ts
191
+ // Before — bonfire
192
+ const options = new ParticleOptions();
193
+ const colorAffector = new ColorAffector(new Color(194, 64, 30, 1), new Color(0, 0, 0, 0));
194
+ const emitter = new UniversalEmitter(50, options);
195
+ const system = new ParticleSystem(texture);
196
+ system.addAffector(colorAffector);
197
+ system.addEmitter(emitter);
198
+
199
+ // in update():
200
+ options.totalLifetime.copy(seconds(rand(5, 10)));
201
+ options.position.set(rand(-50, 50), rand(-10, 10));
202
+ options.velocity.set(/* ... */);
203
+
204
+ // After — bonfire
205
+ const system = new ParticleSystem(texture);
206
+ system.addSpawnModule(new RateSpawn({
207
+ rate: new Constant(50),
208
+ lifetime: new Range(5, 10),
209
+ position: new VectorRange(-50, 50, -10, 10),
210
+ velocity: new ConeDirection(-Math.PI / 2, Math.PI / 36, 60, 80),
211
+ }));
212
+ system.addUpdateModule(new ColorOverLifetime(new Gradient([
213
+ { t: 0, color: new Color(194, 64, 30, 1) },
214
+ { t: 1, color: new Color(0, 0, 0, 0) },
215
+ ])));
216
+ // no per-frame mutation needed.
217
+ ```
218
+
219
+ ```ts
220
+ // Before — gravity affector
221
+ const gravity = new ForceAffector(0, 980);
222
+ system.addAffector(gravity);
223
+
224
+ // After
225
+ system.addUpdateModule(new ApplyForce(0, 980));
226
+ ```
227
+
228
+ ```ts
229
+ // Before — custom affector
230
+ class AlphaFade {
231
+ apply(particle, delta) {
232
+ particle.tint.a = particle.remainingRatio;
233
+ return this;
234
+ }
235
+ destroy() {}
236
+ }
237
+
238
+ // After
239
+ class AlphaFadeOverLifetime extends UpdateModule {
240
+ apply(system) {
241
+ const { color, elapsed, lifetime, liveCount } = system;
242
+ for (let i = 0; i < liveCount; i++) {
243
+ const remaining = 1 - elapsed[i] / lifetime[i];
244
+ const a = (Math.max(0, Math.min(1, remaining)) * 255) & 255;
245
+ color[i] = (color[i] & 0x00ffffff) | (a << 24);
246
+ }
247
+ }
248
+ }
249
+ ```
250
+
251
+ ```ts
252
+ // Before — direct particle creation in tests
253
+ const particle = system.requestParticle();
254
+ particle.position.set(10, 12);
255
+ particle.tint = Color.red;
256
+ system.emitParticle(particle);
257
+
258
+ // After — direct slot manipulation
259
+ const slot = system.spawn();
260
+ system.posX[slot] = 10;
261
+ system.posY[slot] = 12;
262
+ system.color[slot] = Color.red.toRgba();
263
+ system.lifetime[slot] = 1;
264
+ system.scaleX[slot] = 1;
265
+ system.scaleY[slot] = 1;
266
+ ```
267
+
268
+ ### Changed — `ParticleSystem` constructor: typed overloads (BREAKING)
269
+
270
+ Source material (texture / atlas frames / spritesheet) lives in
271
+ **positional arguments** — TypeScript overload signatures enforce mutual
272
+ exclusivity at compile time so you can't pass nonsense combinations like
273
+ texture-and-spritesheet-at-once. Capacity and the test-only `device`
274
+ escape hatch live in the trailing options object.
275
+
276
+ ```ts
277
+ // 0.7.x:
278
+ new ParticleSystem(texture);
279
+ new ParticleSystem(texture, 4096);
280
+
281
+ // 0.8.0:
282
+ new ParticleSystem(); // untextured (1×1 white), CPU/GPU auto-routed
283
+ new ParticleSystem(spark); // simple textured particles
284
+ new ParticleSystem(spark, { capacity: 8192 }); // explicit capacity
285
+ new ParticleSystem(atlas, [r0, r1, r2]); // multi-frame atlas
286
+ new ParticleSystem(atlas, frames, { capacity: 8192 }); // atlas + capacity
287
+ new ParticleSystem(sheet); // spritesheet shorthand
288
+ new ParticleSystem(sheet, { capacity: 4096 });
289
+ ```
290
+
291
+ The four overload signatures:
292
+
293
+ ```ts
294
+ constructor(options?: ParticleSystemOptions);
295
+ constructor(texture: Texture, options?: ParticleSystemOptions);
296
+ constructor(texture: Texture, frames: ReadonlyArray<Rectangle>, options?: ParticleSystemOptions);
297
+ constructor(spritesheet: Spritesheet, options?: ParticleSystemOptions);
298
+ ```
299
+
300
+ Compile-time errors for illegal combinations:
301
+
302
+ ```ts
303
+ new ParticleSystem(spark, sheet); // ✗ no overload matches
304
+ new ParticleSystem(sheet, frames); // ✗ frames only valid with Texture
305
+ new ParticleSystem({ frames }); // ✗ frames isn't an option
306
+ ```
307
+
308
+ **No `backend` option** — the renderer auto-discovers the active backend
309
+ on the first `render(backend)` call. WebGPU → GPU compute path, WebGL2 →
310
+ CPU path. Re-discovery on backend change (device-loss recovery).
311
+
312
+ ### Added — Optional texture + 1×1 white default
313
+
314
+ When `texture` is omitted, the system uses a lazily-allocated 1×1
315
+ opaque-white singleton. Particles render as solid color quads driven by
316
+ the per-particle `color` channel. Useful for tech-demo magic effects,
317
+ abstract VFX, performance benchmarks.
318
+
319
+ ### Added — Multi-frame atlas via `frames` / `spritesheet` options
320
+
321
+ `frames: ReadonlyArray<Rectangle>` declares per-particle frame
322
+ rectangles within the atlas texture. Each particle's `textureIndex[i]`
323
+ selects which frame to render — `RateSpawn` /
324
+ `BurstSpawn`'s `textureIndex: Distribution<number>` becomes the per-spawn
325
+ frame chooser:
326
+
327
+ ```ts
328
+ const system = new ParticleSystem({
329
+ texture: explosionAtlas,
330
+ frames: [
331
+ new Rectangle(0, 0, 32, 32), // index 0 — flame core
332
+ new Rectangle(32, 0, 32, 32), // index 1 — smoke ring
333
+ new Rectangle(64, 0, 32, 32), // index 2 — ember
334
+ ],
335
+ });
336
+
337
+ system.addSpawnModule(new BurstSpawn({
338
+ schedule: [{ time: 0, count: 60 }],
339
+ velocity: ConeDirection.omni(120, 280),
340
+ textureIndex: new Range(0, 2), // each spawn picks a random frame
341
+ }));
342
+ ```
343
+
344
+ `Spritesheet` integration via `spritesheet: sheet` extracts texture +
345
+ frames in insertion order — convenient for atlas authors who already
346
+ have a sheet from a TexturePacker / Aseprite export.
347
+
348
+ UV resolution happens once per particle per frame (CPU pack in CPU mode,
349
+ compute shader in GPU mode); the renderer reads pre-resolved UVs from
350
+ the instance buffer — no shader-side frame-array lookup overhead.
351
+
352
+ ### Changed — Per-instance vertex layout: 24 → 40 bytes
353
+
354
+ The renderer's per-instance buffer now carries `uvMin: vec2` and
355
+ `uvMax: vec2` alongside the existing translation/scale/rotation/color
356
+ fields. Lets a single batch render any mix of atlas frames per instance
357
+ without indirection through a uniform array. Net cost: +67% bandwidth
358
+ on the instance buffer (still trivial — ~10 MB/s at 60 fps with 16k
359
+ particles).
360
+
361
+ The previous design used a single `u_uvBounds` uniform that pinned
362
+ every particle in a system to the same frame; the new layout is what
363
+ makes per-particle atlas selection free.
364
+
365
+ The system pre-allocates all SoA arrays at construction. Spawn modules
366
+ that want to emit beyond capacity get `-1` from `spawn()` and should
367
+ bail cleanly (the built-ins do).
368
+
369
+ ### Changed — slot allocation differs between CPU and GPU mode
370
+
371
+ In CPU mode, `[0, liveCount)` is dense (forward-compaction at end of
372
+ update). `spawn()` always returns the next sequential slot.
373
+
374
+ In GPU mode, no compaction happens — readback would be required to
375
+ move slots whose authoritative position lives in GPU memory. Instead:
376
+ - Each particle has an `alive: Uint8Array` flag (1 = alive, 0 = dead).
377
+ - `spawn()` finds the first dead slot via a round-robin hint pointer
378
+ (amortised O(1), worst case O(capacity)).
379
+ - Expiry on CPU: `alive[i] = 0`, `lifetime[i] = -1` (sentinel).
380
+ - The compute shader skips dead slots (`timing[idx].y < 0.0` → write
381
+ zero-scale instance and return).
382
+
383
+ Custom modules iterating `[0, liveCount)` should check `system.alive[i]`
384
+ in GPU mode if they care about ignoring dead slots; mutating dead slot
385
+ data is harmless because the GPU shader skips them.
386
+
387
+ ### Added — `system.aliveCount`
388
+
389
+ Returns the actual count of alive particles (slots with `alive[i] === 1`).
390
+ In CPU mode this equals `liveCount`; in GPU mode it's `≤ liveCount`.
391
+ Use for fragmentation diagnostics or UI counters.
392
+
393
+ ### Performance notes
394
+
395
+ - Spawning + integrating + ColorOverLifetime/ScaleOverLifetime + drag
396
+ on 10k particles: previously ~5 ms CPU per frame; new SoA path on
397
+ CPU: ~0.5 ms (~10× speedup from eliminating per-particle object
398
+ indirection). New GPU path on WebGPU: ~0.05 ms (~100× speedup from
399
+ the previous OO baseline) — bound by the per-frame upload, not the
400
+ compute itself.
401
+ - The crossover where GPU beats CPU sits around 1-3 k particles
402
+ depending on hardware. For sub-1k systems CPU is still slightly
403
+ faster (upload overhead dominates); the auto-router doesn't second-
404
+ guess this — opt out via `backend: undefined` if you want to force
405
+ CPU at low counts.
406
+ - 100k+ particles render and simulate cleanly on WebGPU at 60 fps in
407
+ CI smoke tests; the bottleneck shifts from compute to texture
408
+ bandwidth at that scale.
409
+
7
410
  ## [0.7.13] - 2026-05-07
8
411
 
9
412
  Major gamepad-input refactor. Replaces the `new Input(...)` +
package/dist/esm/index.js CHANGED
@@ -83,14 +83,35 @@ export { Size } from './math/Size.js';
83
83
  export { PolarVector } from './math/PolarVector.js';
84
84
  export { substepSweep, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst } from './math/swept-collision.js';
85
85
  export { Quadtree } from './math/Quadtree.js';
86
- export { ColorAffector } from './particles/affectors/ColorAffector.js';
87
- export { ForceAffector } from './particles/affectors/ForceAffector.js';
88
- export { ScaleAffector } from './particles/affectors/ScaleAffector.js';
89
- export { TorqueAffector } from './particles/affectors/TorqueAffector.js';
90
- export { ParticleOptions } from './particles/emitters/ParticleOptions.js';
91
- export { UniversalEmitter } from './particles/emitters/UniversalEmitter.js';
92
- export { Particle } from './particles/Particle.js';
93
86
  export { ParticleSystem } from './particles/ParticleSystem.js';
87
+ export { Constant } from './particles/distributions/Constant.js';
88
+ export { Range } from './particles/distributions/Range.js';
89
+ export { VectorRange } from './particles/distributions/VectorRange.js';
90
+ export { ConeDirection } from './particles/distributions/ConeDirection.js';
91
+ export { CircleArea } from './particles/distributions/CircleArea.js';
92
+ export { BoxArea } from './particles/distributions/BoxArea.js';
93
+ export { LineSegment } from './particles/distributions/LineSegment.js';
94
+ export { Curve } from './particles/distributions/Curve.js';
95
+ export { Gradient } from './particles/distributions/Gradient.js';
96
+ export { SpawnModule } from './particles/modules/SpawnModule.js';
97
+ export { UpdateModule } from './particles/modules/UpdateModule.js';
98
+ export { DeathModule } from './particles/modules/DeathModule.js';
99
+ export { wgslFieldLayout, wgslUniformByteSize } from './particles/modules/WgslContribution.js';
100
+ export { RateSpawn } from './particles/modules/RateSpawn.js';
101
+ export { BurstSpawn } from './particles/modules/BurstSpawn.js';
102
+ export { ApplyForce } from './particles/modules/ApplyForce.js';
103
+ export { Drag } from './particles/modules/Drag.js';
104
+ export { ColorOverLifetime } from './particles/modules/ColorOverLifetime.js';
105
+ export { ColorOverSpeed } from './particles/modules/ColorOverSpeed.js';
106
+ export { ScaleOverLifetime } from './particles/modules/ScaleOverLifetime.js';
107
+ export { RotateOverLifetime } from './particles/modules/RotateOverLifetime.js';
108
+ export { AlphaFadeOverLifetime } from './particles/modules/AlphaFadeOverLifetime.js';
109
+ export { VelocityOverLifetime } from './particles/modules/VelocityOverLifetime.js';
110
+ export { AttractToPoint } from './particles/modules/AttractToPoint.js';
111
+ export { RepelFromPoint } from './particles/modules/RepelFromPoint.js';
112
+ export { OrbitalForce } from './particles/modules/OrbitalForce.js';
113
+ export { Turbulence } from './particles/modules/Turbulence.js';
114
+ export { SpawnOnDeath } from './particles/modules/SpawnOnDeath.js';
94
115
  export { Mesh } from './rendering/mesh/Mesh.js';
95
116
  export { Graphics } from './rendering/primitives/Graphics.js';
96
117
  export { Shader } from './rendering/shader/Shader.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}