@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.
- package/CHANGELOG.md +403 -0
- package/dist/esm/index.js +28 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/particles/ParticleSystem.d.ts +180 -83
- package/dist/esm/particles/ParticleSystem.js +446 -133
- package/dist/esm/particles/ParticleSystem.js.map +1 -1
- package/dist/esm/particles/distributions/BoxArea.d.ts +17 -0
- package/dist/esm/particles/distributions/BoxArea.js +48 -0
- package/dist/esm/particles/distributions/BoxArea.js.map +1 -0
- package/dist/esm/particles/distributions/CircleArea.d.ts +19 -0
- package/dist/esm/particles/distributions/CircleArea.js +33 -0
- package/dist/esm/particles/distributions/CircleArea.js.map +1 -0
- package/dist/esm/particles/distributions/ConeDirection.d.ts +28 -0
- package/dist/esm/particles/distributions/ConeDirection.js +44 -0
- package/dist/esm/particles/distributions/ConeDirection.js.map +1 -0
- package/dist/esm/particles/distributions/Constant.d.ts +17 -0
- package/dist/esm/particles/distributions/Constant.js +35 -0
- package/dist/esm/particles/distributions/Constant.js.map +1 -0
- package/dist/esm/particles/distributions/Curve.d.ts +30 -0
- package/dist/esm/particles/distributions/Curve.js +53 -0
- package/dist/esm/particles/distributions/Curve.js.map +1 -0
- package/dist/esm/particles/distributions/Distribution.d.ts +45 -0
- package/dist/esm/particles/distributions/Gradient.d.ts +40 -0
- package/dist/esm/particles/distributions/Gradient.js +72 -0
- package/dist/esm/particles/distributions/Gradient.js.map +1 -0
- package/dist/esm/particles/distributions/LineSegment.d.ts +15 -0
- package/dist/esm/particles/distributions/LineSegment.js +27 -0
- package/dist/esm/particles/distributions/LineSegment.js.map +1 -0
- package/dist/esm/particles/distributions/Range.d.ts +12 -0
- package/dist/esm/particles/distributions/Range.js +19 -0
- package/dist/esm/particles/distributions/Range.js.map +1 -0
- package/dist/esm/particles/distributions/VectorRange.d.ts +20 -0
- package/dist/esm/particles/distributions/VectorRange.js +31 -0
- package/dist/esm/particles/distributions/VectorRange.js.map +1 -0
- package/dist/esm/particles/distributions/index.d.ts +12 -0
- package/dist/esm/particles/gpu/ParticleGpuState.d.ts +57 -0
- package/dist/esm/particles/gpu/ParticleGpuState.js +508 -0
- package/dist/esm/particles/gpu/ParticleGpuState.js.map +1 -0
- package/dist/esm/particles/index.d.ts +2 -10
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +24 -0
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +60 -0
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ApplyForce.d.ts +20 -0
- package/dist/esm/particles/modules/ApplyForce.js +48 -0
- package/dist/esm/particles/modules/ApplyForce.js.map +1 -0
- package/dist/esm/particles/modules/AttractToPoint.d.ts +27 -0
- package/dist/esm/particles/modules/AttractToPoint.js +73 -0
- package/dist/esm/particles/modules/AttractToPoint.js.map +1 -0
- package/dist/esm/particles/modules/BurstSpawn.d.ts +53 -0
- package/dist/esm/particles/modules/BurstSpawn.js +94 -0
- package/dist/esm/particles/modules/BurstSpawn.js.map +1 -0
- package/dist/esm/particles/modules/ColorOverLifetime.d.ts +22 -0
- package/dist/esm/particles/modules/ColorOverLifetime.js +65 -0
- package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ColorOverSpeed.d.ts +27 -0
- package/dist/esm/particles/modules/ColorOverSpeed.js +86 -0
- package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -0
- package/dist/esm/particles/modules/DeathModule.d.ts +24 -0
- package/dist/esm/particles/modules/DeathModule.js +25 -0
- package/dist/esm/particles/modules/DeathModule.js.map +1 -0
- package/dist/esm/particles/modules/Drag.d.ts +20 -0
- package/dist/esm/particles/modules/Drag.js +45 -0
- package/dist/esm/particles/modules/Drag.js.map +1 -0
- package/dist/esm/particles/modules/OrbitalForce.d.ts +28 -0
- package/dist/esm/particles/modules/OrbitalForce.js +65 -0
- package/dist/esm/particles/modules/OrbitalForce.js.map +1 -0
- package/dist/esm/particles/modules/RateSpawn.d.ts +41 -0
- package/dist/esm/particles/modules/RateSpawn.js +76 -0
- package/dist/esm/particles/modules/RateSpawn.js.map +1 -0
- package/dist/esm/particles/modules/RepelFromPoint.d.ts +24 -0
- package/dist/esm/particles/modules/RepelFromPoint.js +76 -0
- package/dist/esm/particles/modules/RepelFromPoint.js.map +1 -0
- package/dist/esm/particles/modules/RotateOverLifetime.d.ts +20 -0
- package/dist/esm/particles/modules/RotateOverLifetime.js +43 -0
- package/dist/esm/particles/modules/RotateOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.d.ts +26 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.js +59 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/SpawnModule.d.ts +30 -0
- package/dist/esm/particles/modules/SpawnModule.js +31 -0
- package/dist/esm/particles/modules/SpawnModule.js.map +1 -0
- package/dist/esm/particles/modules/SpawnOnDeath.d.ts +24 -0
- package/dist/esm/particles/modules/SpawnOnDeath.js +47 -0
- package/dist/esm/particles/modules/SpawnOnDeath.js.map +1 -0
- package/dist/esm/particles/modules/Turbulence.d.ts +30 -0
- package/dist/esm/particles/modules/Turbulence.js +122 -0
- package/dist/esm/particles/modules/Turbulence.js.map +1 -0
- package/dist/esm/particles/modules/UpdateModule.d.ts +95 -0
- package/dist/esm/particles/modules/UpdateModule.js +66 -0
- package/dist/esm/particles/modules/UpdateModule.js.map +1 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.d.ts +30 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.js +84 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/WgslContribution.d.ts +81 -0
- package/dist/esm/particles/modules/WgslContribution.js +34 -0
- package/dist/esm/particles/modules/WgslContribution.js.map +1 -0
- package/dist/esm/particles/modules/index.d.ts +22 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +9 -14
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +90 -61
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +9 -0
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +107 -23
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/compute/WebGpuComputePipeline.d.ts +52 -0
- package/dist/esm/rendering/webgpu/compute/WebGpuStorageBuffer.d.ts +29 -0
- package/dist/esm/rendering/webgpu/compute/index.d.ts +3 -0
- package/dist/exo.esm.js +2657 -742
- package/dist/exo.esm.js.map +1 -1
- package/package.json +1 -1
- package/dist/esm/particles/Particle.d.ts +0 -77
- package/dist/esm/particles/Particle.js +0 -143
- package/dist/esm/particles/Particle.js.map +0 -1
- package/dist/esm/particles/ParticleProperties.d.ts +0 -29
- package/dist/esm/particles/affectors/ColorAffector.d.ts +0 -30
- package/dist/esm/particles/affectors/ColorAffector.js +0 -55
- package/dist/esm/particles/affectors/ColorAffector.js.map +0 -1
- package/dist/esm/particles/affectors/ForceAffector.d.ts +0 -24
- package/dist/esm/particles/affectors/ForceAffector.js +0 -39
- package/dist/esm/particles/affectors/ForceAffector.js.map +0 -1
- package/dist/esm/particles/affectors/ParticleAffector.d.ts +0 -19
- package/dist/esm/particles/affectors/ScaleAffector.d.ts +0 -23
- package/dist/esm/particles/affectors/ScaleAffector.js +0 -38
- package/dist/esm/particles/affectors/ScaleAffector.js.map +0 -1
- package/dist/esm/particles/affectors/TorqueAffector.d.ts +0 -23
- package/dist/esm/particles/affectors/TorqueAffector.js +0 -37
- package/dist/esm/particles/affectors/TorqueAffector.js.map +0 -1
- package/dist/esm/particles/emitters/ParticleEmitter.d.ts +0 -19
- package/dist/esm/particles/emitters/ParticleOptions.d.ts +0 -62
- package/dist/esm/particles/emitters/ParticleOptions.js +0 -120
- package/dist/esm/particles/emitters/ParticleOptions.js.map +0 -1
- package/dist/esm/particles/emitters/UniversalEmitter.d.ts +0 -40
- package/dist/esm/particles/emitters/UniversalEmitter.js +0 -68
- 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';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|