@flighthq/particles 0.1.0 → 0.2.1-next.410.a23f189

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 (55) hide show
  1. package/dist/applyParticleCollisions.d.ts +2 -2
  2. package/dist/applyParticleCollisions.d.ts.map +1 -1
  3. package/dist/applyParticleCollisions.js +75 -30
  4. package/dist/applyParticleCollisions.js.map +1 -1
  5. package/dist/applyParticleForces.d.ts.map +1 -1
  6. package/dist/applyParticleForces.js +33 -10
  7. package/dist/applyParticleForces.js.map +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/particleEmitterConfig.d.ts.map +1 -1
  13. package/dist/particleEmitterConfig.js +4 -0
  14. package/dist/particleEmitterConfig.js.map +1 -1
  15. package/dist/particleEmitterState.d.ts +1 -0
  16. package/dist/particleEmitterState.d.ts.map +1 -1
  17. package/dist/particleEmitterState.js +4 -1
  18. package/dist/particleEmitterState.js.map +1 -1
  19. package/dist/stepParticleObjects.d.ts +19 -0
  20. package/dist/stepParticleObjects.d.ts.map +1 -0
  21. package/dist/stepParticleObjects.js +29 -0
  22. package/dist/stepParticleObjects.js.map +1 -0
  23. package/dist/validateParticleEmitterConfig.d.ts.map +1 -1
  24. package/dist/validateParticleEmitterConfig.js +8 -0
  25. package/dist/validateParticleEmitterConfig.js.map +1 -1
  26. package/package.json +10 -7
  27. package/src/applyParticleCollisions.test.ts +71 -32
  28. package/src/applyParticleForces.test.ts +70 -34
  29. package/src/curve.test.ts +0 -63
  30. package/src/particleEmitterConfig.test.ts +4 -0
  31. package/src/particleEmitterSignals.test.ts +0 -83
  32. package/src/particleEmitterState.test.ts +7 -44
  33. package/src/stepParticleObjects.test.ts +30 -0
  34. package/src/validateParticleEmitterConfig.test.ts +0 -32
  35. package/dist/emitParticleBurst.d.ts +0 -15
  36. package/dist/emitParticleBurst.d.ts.map +0 -1
  37. package/dist/emitParticleBurst.js +0 -114
  38. package/dist/emitParticleBurst.js.map +0 -1
  39. package/dist/prewarmParticleEmitter.d.ts +0 -5
  40. package/dist/prewarmParticleEmitter.d.ts.map +0 -1
  41. package/dist/prewarmParticleEmitter.js +0 -13
  42. package/dist/prewarmParticleEmitter.js.map +0 -1
  43. package/dist/stepParticleEmitter.d.ts +0 -38
  44. package/dist/stepParticleEmitter.d.ts.map +0 -1
  45. package/dist/stepParticleEmitter.js +0 -57
  46. package/dist/stepParticleEmitter.js.map +0 -1
  47. package/dist/updateParticleEmitter.d.ts +0 -8
  48. package/dist/updateParticleEmitter.d.ts.map +0 -1
  49. package/dist/updateParticleEmitter.js +0 -315
  50. package/dist/updateParticleEmitter.js.map +0 -1
  51. package/src/deterministic.test.ts +0 -54
  52. package/src/emitParticleBurst.test.ts +0 -103
  53. package/src/prewarmParticleEmitter.test.ts +0 -68
  54. package/src/stepParticleEmitter.test.ts +0 -113
  55. package/src/updateParticleEmitter.test.ts +0 -631
@@ -21,6 +21,9 @@ const NUMERIC_FIELDS = [
21
21
  'colorStartVarianceR',
22
22
  'directionX',
23
23
  'directionY',
24
+ 'directionZ',
25
+ 'emitterConeAngle',
26
+ 'emitterDepth',
24
27
  'emitterHeight',
25
28
  'emitterRadius',
26
29
  'emitterWidth',
@@ -28,6 +31,7 @@ const NUMERIC_FIELDS = [
28
31
  'frameRate',
29
32
  'gravityX',
30
33
  'gravityY',
34
+ 'gravityZ',
31
35
  'lifetimeMax',
32
36
  'lifetimeMin',
33
37
  'maxParticles',
@@ -50,6 +54,8 @@ const NON_NEGATIVE_FIELDS = [
50
54
  'burstCount',
51
55
  'burstInterval',
52
56
  'duration',
57
+ 'emitterConeAngle',
58
+ 'emitterDepth',
53
59
  'emitterHeight',
54
60
  'emitterRadius',
55
61
  'emitterWidth',
@@ -101,6 +107,8 @@ export function normalizeParticleEmitterConfig(config) {
101
107
  speedMax: Math.max(0, out.speedMax),
102
108
  scaleMin: Math.max(0, out.scaleMin),
103
109
  scaleMax: Math.max(0, out.scaleMax),
110
+ emitterConeAngle: Math.max(0, out.emitterConeAngle),
111
+ emitterDepth: Math.max(0, out.emitterDepth),
104
112
  emitterRadius: Math.max(0, out.emitterRadius),
105
113
  emitterWidth: Math.max(0, out.emitterWidth),
106
114
  emitterHeight: Math.max(0, out.emitterHeight),
@@ -1 +1 @@
1
- {"version":3,"file":"validateParticleEmitterConfig.js","sourceRoot":"","sources":["../src/validateParticleEmitterConfig.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validateParticleEmitterConfig.js","sourceRoot":"","sources":["../src/validateParticleEmitterConfig.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@flighthq/particles",
3
- "version": "0.1.0",
3
+ "version": "0.2.1-next.410.a23f189",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/particles"
8
+ },
4
9
  "type": "module",
5
10
  "main": "dist/index.js",
6
11
  "types": "dist/index.d.ts",
@@ -27,14 +32,12 @@
27
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
33
  },
29
34
  "dependencies": {
30
- "@flighthq/geometry": "0.1.0",
31
- "@flighthq/math": "0.1.0",
32
- "@flighthq/node": "0.1.0",
33
- "@flighthq/signals": "0.1.0",
34
- "@flighthq/sprite": "0.1.0",
35
- "@flighthq/types": "0.1.0"
35
+ "@flighthq/geometry": "0.2.1-next.410.a23f189",
36
+ "@flighthq/signals": "0.2.1-next.410.a23f189",
37
+ "@flighthq/types": "0.2.1-next.410.a23f189"
36
38
  },
37
39
  "devDependencies": {
40
+ "@flighthq/math": "*",
38
41
  "typescript": "^5.3.0"
39
42
  },
40
43
  "description": "Particle system: emitter simulation and typed-array-backed particle primitives",
@@ -1,32 +1,33 @@
1
- import { createParticleEmitter } from '@flighthq/sprite';
2
- import type { TextureAtlas } from '@flighthq/types';
1
+ import type { ParticleEmitter, ParticleEmitterData } from '@flighthq/types';
3
2
 
4
3
  import { applyParticleCollisions, applyParticleObjectCollisions } from './applyParticleCollisions';
5
- import { createParticleEmitterConfig } from './particleEmitterConfig';
6
- import { createParticleEmitterState } from './particleEmitterState';
4
+ import { createParticleEmitterState, ensureParticleEmitterStateCapacity } from './particleEmitterState';
7
5
  import { createParticleObjectsState } from './particleObjectsState';
8
- import { updateParticleEmitter } from './updateParticleEmitter';
9
6
  import type { ParticleObject } from './updateParticleObjects';
10
7
 
11
- function makeAtlas(): TextureAtlas {
12
- return {
13
- image: null,
14
- regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
15
- } as TextureAtlas;
8
+ // Node-free SoA emitter fixture: applyParticleCollisions reads only `emitter.data`, so the display
9
+ // node from @flighthq/particleemitter is not needed to unit-test the collision pass.
10
+ function createEmitterFixture(capacity: number): ParticleEmitter {
11
+ const data: ParticleEmitterData = {
12
+ alphas: new Float32Array(capacity),
13
+ atlas: null,
14
+ colors: new Float32Array(capacity * 3),
15
+ ids: new Uint16Array(capacity),
16
+ particleCount: 0,
17
+ positionsZ: new Float32Array(capacity),
18
+ transforms: new Float32Array(capacity * 4),
19
+ velocities: new Float32Array(capacity * 2),
20
+ worldSpace: false,
21
+ };
22
+ return { data, x: 0, y: 0 } as unknown as ParticleEmitter;
16
23
  }
17
24
 
18
- // Spawn one particle, then place it at a known position with a known velocity.
25
+ // One particle at a known position with a known velocity.
19
26
  function place(px: number, py: number, vx: number, vy: number) {
20
- const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
27
+ const emitter = createEmitterFixture(1);
21
28
  const state = createParticleEmitterState();
22
- const config = createParticleEmitterConfig({
23
- spawnRate: 1,
24
- lifetimeMin: 100,
25
- lifetimeMax: 100,
26
- speedMin: 0,
27
- speedMax: 0,
28
- });
29
- updateParticleEmitter(emitter, state, config, 1);
29
+ ensureParticleEmitterStateCapacity(state, 1, false);
30
+ emitter.data.particleCount = 1;
30
31
  emitter.data.transforms[0] = px;
31
32
  emitter.data.transforms[1] = py;
32
33
  state.velocities[0] = vx;
@@ -55,18 +56,6 @@ describe('applyParticleCollisions', () => {
55
56
  });
56
57
 
57
58
  describe('integration', () => {
58
- it('settles a falling particle onto a floor over multiple frames', () => {
59
- const { emitter, state } = place(0, 480, 0, 0);
60
- const config = createParticleEmitterConfig({ spawnRate: 0, lifetimeMin: 100, lifetimeMax: 100, gravityY: 2000 });
61
- const floor = [{ kind: 'PlaneCollider' as const, nx: 0, ny: -1, distance: -500 }];
62
- for (let i = 0; i < 60; i++) {
63
- updateParticleEmitter(emitter, state, config, 1 / 60);
64
- applyParticleCollisions(emitter, state, floor);
65
- }
66
- expect(emitter.data.transforms[1]).toBeLessThanOrEqual(500 + 1e-3); // never falls through
67
- expect(emitter.data.transforms[1]).toBeCloseTo(500, 0); // resting on the floor
68
- });
69
-
70
59
  it('is a no-op with no colliders', () => {
71
60
  const { emitter, state } = place(0, 520, 0, 100);
72
61
  applyParticleCollisions(emitter, state, []);
@@ -107,6 +96,56 @@ describe('applyParticleCollisions', () => {
107
96
  });
108
97
  });
109
98
 
99
+ describe('plane (3D normal)', () => {
100
+ it('plane with nz pushes a penetrating particle back along the 3D normal', () => {
101
+ // Plane with normal pointing in -z, distance -100 → valid region z <= 100
102
+ const { emitter, state } = place(0, 0, 0, 0);
103
+ emitter.data.positionsZ[0] = 120; // 20 units past the plane, moving +z
104
+ state.velocities[2] = 50;
105
+ applyParticleCollisions(emitter, state, [{ kind: 'PlaneCollider', nx: 0, ny: 0, nz: -1, distance: -100 }]);
106
+ expect(emitter.data.positionsZ[0]).toBeCloseTo(100); // snapped to surface
107
+ expect(state.velocities[2]).toBeCloseTo(0); // normal velocity killed (restitution 0)
108
+ });
109
+ });
110
+
111
+ describe('sphere', () => {
112
+ it('exclude mode keeps a particle outside the sphere', () => {
113
+ const { emitter, state } = place(3, 4, -10, 0); // inside radius-10 sphere at origin
114
+ emitter.data.positionsZ[0] = 0;
115
+ state.velocities[2] = 0;
116
+ applyParticleCollisions(emitter, state, [
117
+ { kind: 'SphereCollider', x: 0, y: 0, z: 0, radius: 10, mode: 'exclude', restitution: 1 },
118
+ ]);
119
+ const d = Math.sqrt(
120
+ emitter.data.transforms[0] ** 2 + emitter.data.transforms[1] ** 2 + emitter.data.positionsZ[0] ** 2,
121
+ );
122
+ expect(d).toBeCloseTo(10); // pushed to the surface
123
+ });
124
+
125
+ it('contain mode keeps a particle inside the sphere', () => {
126
+ const { emitter, state } = place(20, 0, 10, 0); // outside radius-10 sphere
127
+ emitter.data.positionsZ[0] = 0;
128
+ state.velocities[2] = 0;
129
+ applyParticleCollisions(emitter, state, [
130
+ { kind: 'SphereCollider', x: 0, y: 0, z: 0, radius: 10, mode: 'contain' },
131
+ ]);
132
+ const d = Math.sqrt(
133
+ emitter.data.transforms[0] ** 2 + emitter.data.transforms[1] ** 2 + emitter.data.positionsZ[0] ** 2,
134
+ );
135
+ expect(d).toBeCloseTo(10);
136
+ });
137
+
138
+ it('sphere contain with 3D offset', () => {
139
+ const { emitter, state } = place(0, 0, 0, 0);
140
+ emitter.data.positionsZ[0] = 25; // 25 units from center at (0,0,0), outside radius 10
141
+ state.velocities[2] = 10;
142
+ applyParticleCollisions(emitter, state, [
143
+ { kind: 'SphereCollider', x: 0, y: 0, z: 0, radius: 10, mode: 'contain' },
144
+ ]);
145
+ expect(emitter.data.positionsZ[0]).toBeCloseTo(10); // clamped to surface
146
+ });
147
+ });
148
+
110
149
  describe('rect', () => {
111
150
  it('contain mode clamps particles to the bounds and reflects velocity', () => {
112
151
  // Box centered (0,0), 100×100 → bounds ±50.
@@ -1,33 +1,36 @@
1
- import { createParticleEmitter } from '@flighthq/sprite';
2
- import type { TextureAtlas } from '@flighthq/types';
1
+ import type { ParticleEmitter, ParticleEmitterData } from '@flighthq/types';
3
2
 
4
3
  import { applyParticleForces, applyParticleObjectForces } from './applyParticleForces';
5
4
  import { createParticleEmitterConfig } from './particleEmitterConfig';
6
- import { createParticleEmitterState } from './particleEmitterState';
5
+ import { createParticleEmitterState, ensureParticleEmitterStateCapacity } from './particleEmitterState';
7
6
  import { createParticleObjectsState } from './particleObjectsState';
8
- import { updateParticleEmitter } from './updateParticleEmitter';
9
7
  import type { ParticleObject } from './updateParticleObjects';
10
8
  import { updateParticleObjects } from './updateParticleObjects';
11
9
 
12
- function makeAtlas(): TextureAtlas {
13
- return {
14
- image: null,
15
- regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
16
- } as TextureAtlas;
10
+ // Node-free SoA emitter fixture: applyParticleForces reads only `emitter.data`, so the display
11
+ // node from @flighthq/particleemitter is not needed to unit-test the force pass. Keeping this
12
+ // fixture local preserves particles as a pure sim leaf.
13
+ function createEmitterFixture(capacity: number): ParticleEmitter {
14
+ const data: ParticleEmitterData = {
15
+ alphas: new Float32Array(capacity),
16
+ atlas: null,
17
+ colors: new Float32Array(capacity * 3),
18
+ ids: new Uint16Array(capacity),
19
+ particleCount: 0,
20
+ positionsZ: new Float32Array(capacity),
21
+ transforms: new Float32Array(capacity * 4),
22
+ velocities: new Float32Array(capacity * 2),
23
+ worldSpace: false,
24
+ };
25
+ return { data, x: 0, y: 0 } as unknown as ParticleEmitter;
17
26
  }
18
27
 
19
- // Spawn a single stationary particle at the origin and return its handles.
28
+ // A single stationary particle at the origin with zeroed velocity state.
20
29
  function oneParticle() {
21
- const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
30
+ const emitter = createEmitterFixture(1);
22
31
  const state = createParticleEmitterState();
23
- const config = createParticleEmitterConfig({
24
- spawnRate: 1,
25
- lifetimeMin: 100,
26
- lifetimeMax: 100,
27
- speedMin: 0,
28
- speedMax: 0,
29
- });
30
- updateParticleEmitter(emitter, state, config, 1);
32
+ ensureParticleEmitterStateCapacity(state, 1, false);
33
+ emitter.data.particleCount = 1;
31
34
  return { emitter, state };
32
35
  }
33
36
 
@@ -118,22 +121,55 @@ describe('applyParticleForces', () => {
118
121
  expect(state.velocities[0]).toBe(42); // unchanged by either no-op call
119
122
  });
120
123
 
121
- it('integrates with the core update so attractors curve trajectories', () => {
124
+ it('wind z component adds acceleration on the Z axis', () => {
122
125
  const { emitter, state } = oneParticle();
123
- const config = createParticleEmitterConfig({
124
- spawnRate: 0,
125
- speedMin: 0,
126
- speedMax: 0,
127
- lifetimeMin: 100,
128
- lifetimeMax: 100,
129
- });
130
- const forces = [{ kind: 'AttractorForce' as const, x: 100, y: 0, strength: 200 }];
131
- const startX = emitter.data.transforms[0];
132
- for (let i = 0; i < 10; i++) {
133
- applyParticleForces(emitter, state, forces, 1 / 60);
134
- updateParticleEmitter(emitter, state, config, 1 / 60);
135
- }
136
- expect(emitter.data.transforms[0]).toBeGreaterThan(startX); // drifted toward the attractor
126
+ applyParticleForces(emitter, state, [{ kind: 'WindForce', x: 0, y: 0, z: 10 }], 1);
127
+ expect(state.velocities[0]).toBeCloseTo(0);
128
+ expect(state.velocities[1]).toBeCloseTo(0);
129
+ expect(state.velocities[2]).toBeCloseTo(10);
130
+ });
131
+
132
+ it('attractor with z pulls velocity toward the 3D point', () => {
133
+ const { emitter, state } = oneParticle();
134
+ emitter.data.positionsZ[0] = 0;
135
+ applyParticleForces(emitter, state, [{ kind: 'AttractorForce', x: 0, y: 0, z: 100, strength: 50 }], 1);
136
+ expect(state.velocities[0]).toBeCloseTo(0);
137
+ expect(state.velocities[1]).toBeCloseTo(0);
138
+ expect(state.velocities[2]).toBeCloseTo(50); // accelerated +z toward (0,0,100)
139
+ });
140
+
141
+ it('vortex with custom axis rotates around that axis', () => {
142
+ const { emitter, state } = oneParticle();
143
+ // Particle at (10, 0, 0), vortex at origin with axis [1, 0, 0] (X axis)
144
+ emitter.data.transforms[0] = 0;
145
+ emitter.data.transforms[1] = 10;
146
+ emitter.data.positionsZ[0] = 0;
147
+ applyParticleForces(
148
+ emitter,
149
+ state,
150
+ [{ kind: 'VortexForce', x: 0, y: 0, z: 0, strength: 20, axisX: 1, axisY: 0, axisZ: 0 }],
151
+ 1,
152
+ );
153
+ // With axis = [1,0,0], radial = [0,1,0], tangent = axis x radial = [0,0,1]
154
+ expect(state.velocities[0]).toBeCloseTo(0);
155
+ expect(state.velocities[1]).toBeCloseTo(0);
156
+ expect(state.velocities[2]).toBeCloseTo(20); // force in z direction
157
+ });
158
+
159
+ it('turbulence produces a finite z component', () => {
160
+ const { emitter, state } = oneParticle();
161
+ emitter.data.transforms[0] = 5;
162
+ emitter.data.transforms[1] = 5;
163
+ emitter.data.positionsZ[0] = 5;
164
+ applyParticleForces(emitter, state, [{ kind: 'TurbulenceForce', strength: 100, scale: 0.1 }], 1);
165
+ expect(Number.isFinite(state.velocities[2])).toBe(true);
166
+ });
167
+
168
+ it('drag damps z velocity', () => {
169
+ const { emitter, state } = oneParticle();
170
+ state.velocities[2] = 100;
171
+ applyParticleForces(emitter, state, [{ kind: 'DragForce', strength: 0.5 }], 1);
172
+ expect(state.velocities[2]).toBeCloseTo(50); // 100 - 0.5*100
137
173
  });
138
174
  });
139
175
 
package/src/curve.test.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { createParticleEmitter } from '@flighthq/sprite';
2
- import type { TextureAtlas } from '@flighthq/types';
3
-
4
1
  import {
5
2
  buildParticleColorCurve,
6
3
  buildParticleCurve,
@@ -13,16 +10,6 @@ import {
13
10
  sampleParticleColorCurve,
14
11
  sampleParticleCurve,
15
12
  } from './curve';
16
- import { createParticleEmitterConfig } from './particleEmitterConfig';
17
- import { createParticleEmitterState } from './particleEmitterState';
18
- import { updateParticleEmitter } from './updateParticleEmitter';
19
-
20
- function makeAtlas(): TextureAtlas {
21
- return {
22
- image: null,
23
- regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
24
- } as TextureAtlas;
25
- }
26
13
 
27
14
  describe('buildParticleColorCurve', () => {
28
15
  it('bakes an RGB function into an interleaved LUT', () => {
@@ -111,56 +98,6 @@ describe('lerpHsvInPlace', () => {
111
98
  });
112
99
  });
113
100
 
114
- describe('lifetime curves in updateParticleEmitter', () => {
115
- function spawnOne(configOverrides: Parameters<typeof createParticleEmitterConfig>[0]) {
116
- const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
117
- const state = createParticleEmitterState();
118
- const config = createParticleEmitterConfig({
119
- spawnRate: 1,
120
- lifetimeMin: 1,
121
- lifetimeMax: 1,
122
- speedMin: 0,
123
- speedMax: 0,
124
- ...configOverrides,
125
- });
126
- updateParticleEmitter(emitter, state, config, 1); // spawn one particle
127
- return { emitter, state, config };
128
- }
129
-
130
- it('alphaCurve drives alpha non-linearly over lifetime', () => {
131
- // A curve that is 1 at birth, dips to 0 at mid-life, back to 1 at death.
132
- const { emitter, state, config } = spawnOne({ alphaCurve: [1, 0, 1] });
133
- updateParticleEmitter(emitter, state, config, 0.5); // → mid-life
134
- expect(emitter.data.alphas[0]).toBeCloseTo(0, 2);
135
- });
136
-
137
- it('colorCurve overrides the start/end gradient', () => {
138
- // green at birth, red at death (interleaved RGB).
139
- const { emitter, state, config } = spawnOne({
140
- colorCurve: [0, 1, 0, 1, 0, 0],
141
- colorStartR: 0.123, // would be used by the linear path; curve must win
142
- });
143
- updateParticleEmitter(emitter, state, config, 0.5);
144
- expect(emitter.data.colors[0]).toBeCloseTo(0.5); // R halfway
145
- expect(emitter.data.colors[1]).toBeCloseTo(0.5); // G halfway
146
- });
147
-
148
- it('scaleCurve multiplies the spawn scale', () => {
149
- const { emitter, state, config } = spawnOne({
150
- scaleMin: 2,
151
- scaleMax: 2,
152
- scaleCurve: [1, 0.5, 0], // shrink to nothing
153
- });
154
- updateParticleEmitter(emitter, state, config, 0.5); // mid-life → factor 0.5
155
- expect(emitter.data.transforms[3]).toBeCloseTo(1, 2); // 2 * 0.5
156
- });
157
-
158
- it('sets curve-driven values at spawn time (t=0)', () => {
159
- const { emitter } = spawnOne({ alphaCurve: [0.25, 1] });
160
- expect(emitter.data.alphas[0]).toBeCloseTo(0.25, 2);
161
- });
162
- });
163
-
164
101
  describe('particleColorCurveFromKeyframes', () => {
165
102
  it('bakes an RGB timeline through its middle stop', () => {
166
103
  const lut = particleColorCurveFromKeyframes([
@@ -23,8 +23,12 @@ describe('createParticleEmitterConfig', () => {
23
23
  expect(config.emitterHeight).toBe(0);
24
24
  expect(config.frameCount).toBe(1);
25
25
  expect(config.frameRate).toBe(12);
26
+ expect(config.directionZ).toBe(0);
27
+ expect(config.emitterConeAngle).toBe(0);
28
+ expect(config.emitterDepth).toBe(0);
26
29
  expect(config.gravityX).toBe(0);
27
30
  expect(config.gravityY).toBe(0);
31
+ expect(config.gravityZ).toBe(0);
28
32
  expect(config.lifetimeMax).toBe(1);
29
33
  expect(config.lifetimeMin).toBe(0.5);
30
34
  expect(config.maxParticles).toBe(1000);
@@ -1,35 +1,9 @@
1
- import { createParticleEmitter } from '@flighthq/sprite';
2
- import type { TextureAtlas } from '@flighthq/types';
3
-
4
- import { createParticleEmitterConfig } from './particleEmitterConfig';
5
1
  import {
6
2
  createParticleEmitterSignals,
7
3
  enableParticleEmitterSignals,
8
4
  getParticleEmitterSignals,
9
5
  } from './particleEmitterSignals';
10
6
  import { createParticleEmitterState } from './particleEmitterState';
11
- import { updateParticleEmitter } from './updateParticleEmitter';
12
-
13
- function makeAtlas(): TextureAtlas {
14
- return {
15
- image: null,
16
- regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
17
- } as unknown as TextureAtlas;
18
- }
19
-
20
- function spawnOne(configOverrides: Record<string, unknown> = {}) {
21
- const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
22
- const state = createParticleEmitterState();
23
- const config = createParticleEmitterConfig({
24
- spawnRate: 1,
25
- lifetimeMin: 1,
26
- lifetimeMax: 1,
27
- speedMin: 0,
28
- speedMax: 0,
29
- ...configOverrides,
30
- });
31
- return { emitter, state, config };
32
- }
33
7
 
34
8
  describe('createParticleEmitterSignals', () => {
35
9
  it('creates a signals group with all three signal slots', () => {
@@ -66,60 +40,3 @@ describe('getParticleEmitterSignals', () => {
66
40
  expect(getParticleEmitterSignals(state)).toBeNull();
67
41
  });
68
42
  });
69
-
70
- describe('onEmitterComplete signal', () => {
71
- it('fires once when a finite non-looping emitter completes', () => {
72
- const { emitter, state } = spawnOne({ duration: 0.5, loop: false });
73
- const config = createParticleEmitterConfig({
74
- duration: 0.5,
75
- loop: false,
76
- spawnRate: 1,
77
- lifetimeMin: 0.1,
78
- lifetimeMax: 0.1,
79
- speedMin: 0,
80
- speedMax: 0,
81
- });
82
- const signals = enableParticleEmitterSignals(state);
83
- const completes: number[] = [];
84
- signals.onEmitterComplete.emit = () => completes.push(1);
85
- updateParticleEmitter(emitter, state, config, 1); // spawn + age past duration
86
- updateParticleEmitter(emitter, state, config, 1); // particles die → complete
87
- expect(completes.length).toBeGreaterThan(0);
88
- });
89
-
90
- it('does not fire for infinite emitters', () => {
91
- const { emitter, state, config } = spawnOne({ duration: 0, loop: true });
92
- const signals = enableParticleEmitterSignals(state);
93
- const completes: number[] = [];
94
- signals.onEmitterComplete.emit = () => completes.push(1);
95
- for (let i = 0; i < 10; i++) updateParticleEmitter(emitter, state, config, 1);
96
- expect(completes.length).toBe(0);
97
- });
98
- });
99
-
100
- describe('onParticleDeath signal', () => {
101
- it('fires with the particle position when a particle dies', () => {
102
- const { emitter, state, config } = spawnOne();
103
- const signals = enableParticleEmitterSignals(state);
104
- const deaths: Array<[number, number]> = [];
105
- signals.onParticleDeath.emit = (x: number, y: number) => deaths.push([x, y]);
106
- updateParticleEmitter(emitter, state, config, 1); // spawn
107
- updateParticleEmitter(emitter, state, config, 1.1); // age past lifetime → death
108
- expect(deaths.length).toBe(1);
109
- expect(Number.isFinite(deaths[0][0])).toBe(true);
110
- });
111
- });
112
-
113
- describe('onParticleSpawn signal', () => {
114
- it('fires with spawn position and velocity when a particle is spawned', () => {
115
- const { emitter, state, config } = spawnOne({ speedMin: 50, speedMax: 50 });
116
- const signals = enableParticleEmitterSignals(state);
117
- const spawns: Array<[number, number, number, number]> = [];
118
- signals.onParticleSpawn.emit = (x: number, y: number, vx: number, vy: number) => spawns.push([x, y, vx, vy]);
119
- updateParticleEmitter(emitter, state, config, 1);
120
- expect(spawns.length).toBe(1);
121
- const [, , vx, vy] = spawns[0];
122
- // Speed must match config (allow sign variations by checking magnitude).
123
- expect(Math.sqrt(vx * vx + vy * vy)).toBeCloseTo(50, 0);
124
- });
125
- });
@@ -1,48 +1,10 @@
1
- import { createRandomSource } from '@flighthq/math';
2
- import { createParticleEmitter } from '@flighthq/sprite';
3
- import type { TextureAtlas } from '@flighthq/types';
4
-
5
- import { createParticleEmitterConfig } from './particleEmitterConfig';
6
- import { createParticleEmitterState, ensureParticleEmitterStateCapacity } from './particleEmitterState';
7
- import { updateParticleEmitter } from './updateParticleEmitter';
8
-
9
- // Runs a short emitter simulation seeded from `seed` and returns the live particle transforms, so a
10
- // seeded state can be shown to simulate reproducibly (the seeded RNG now lives in @flighthq/math).
11
- function simulate(seed: number): number[] {
12
- const emitter = createParticleEmitter({
13
- data: {
14
- atlas: {
15
- image: null,
16
- regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
17
- } as TextureAtlas,
18
- },
19
- });
20
- const state = createParticleEmitterState(createRandomSource(seed));
21
- const config = createParticleEmitterConfig({
22
- spawnRate: 20,
23
- maxParticles: 100,
24
- lifetimeMin: 0.5,
25
- lifetimeMax: 1.5,
26
- speedMin: 10,
27
- speedMax: 200,
28
- spread: Math.PI,
29
- emitterShape: 'circle',
30
- emitterRadius: 25,
31
- colorStartVarianceR: 0.5,
32
- });
33
- for (let f = 0; f < 30; f++) updateParticleEmitter(emitter, state, config, 1 / 60);
34
- return Array.from(emitter.data.transforms.slice(0, emitter.data.particleCount * 4));
35
- }
1
+ import {
2
+ PARTICLE_VELOCITY_STRIDE,
3
+ createParticleEmitterState,
4
+ ensureParticleEmitterStateCapacity,
5
+ } from './particleEmitterState';
36
6
 
37
7
  describe('createParticleEmitterState', () => {
38
- it('seeded identically, two states simulate bit-for-bit identically', () => {
39
- expect(simulate(42)).toEqual(simulate(42));
40
- });
41
-
42
- it('diverges under different seeds', () => {
43
- expect(simulate(42)).not.toEqual(simulate(43));
44
- });
45
-
46
8
  it('returns empty arrays, zero accumulator, burstTimer=0, and NaN prev position', () => {
47
9
  const state = createParticleEmitterState();
48
10
  expect(state.lifetimes).toBeInstanceOf(Float32Array);
@@ -59,6 +21,7 @@ describe('createParticleEmitterState', () => {
59
21
  expect(state.burstTimer).toBe(0);
60
22
  expect(isNaN(state.prevX)).toBe(true);
61
23
  expect(isNaN(state.prevY)).toBe(true);
24
+ expect(isNaN(state.prevZ)).toBe(true);
62
25
  });
63
26
 
64
27
  it('returns a new object each call', () => {
@@ -73,7 +36,7 @@ describe('ensureParticleEmitterStateCapacity', () => {
73
36
  const state = createParticleEmitterState();
74
37
  ensureParticleEmitterStateCapacity(state, 10, false);
75
38
  expect(state.lifetimes.length).toBeGreaterThanOrEqual(10 * 2);
76
- expect(state.velocities.length).toBeGreaterThanOrEqual(10 * 2);
39
+ expect(state.velocities.length).toBeGreaterThanOrEqual(10 * PARTICLE_VELOCITY_STRIDE);
77
40
  expect(state.scales.length).toBeGreaterThanOrEqual(10);
78
41
  expect(state.rotationSpeeds.length).toBeGreaterThanOrEqual(10);
79
42
  });
@@ -0,0 +1,30 @@
1
+ import type { ParticleObject } from '@flighthq/types';
2
+
3
+ import { createParticleEmitterConfig } from './particleEmitterConfig';
4
+ import { createParticleObjectsState } from './particleObjectsState';
5
+ import { stepParticleObjects } from './stepParticleObjects';
6
+
7
+ function makeObject(): ParticleObject {
8
+ return {
9
+ alpha: 1,
10
+ blendMode: null,
11
+ colorTransform: null,
12
+ rotation: 0,
13
+ scaleX: 1,
14
+ scaleY: 1,
15
+ visible: false,
16
+ x: 0,
17
+ y: 0,
18
+ } as unknown as ParticleObject;
19
+ }
20
+
21
+ describe('stepParticleObjects', () => {
22
+ it('spawns into dead slots when called with no forces or colliders', () => {
23
+ const objects = Array.from({ length: 10 }, makeObject);
24
+ const state = createParticleObjectsState(10);
25
+ const config = createParticleEmitterConfig({ spawnRate: 5, lifetimeMin: 10, lifetimeMax: 10 });
26
+ stepParticleObjects(objects, state, config, 1);
27
+ const visible = objects.filter((o) => o.visible).length;
28
+ expect(visible).toBe(5);
29
+ });
30
+ });