@flighthq/particles 0.1.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/dist/applyParticleCollisions.d.ts +12 -0
- package/dist/applyParticleCollisions.d.ts.map +1 -0
- package/dist/applyParticleCollisions.js +175 -0
- package/dist/applyParticleCollisions.js.map +1 -0
- package/dist/applyParticleForces.d.ts +13 -0
- package/dist/applyParticleForces.d.ts.map +1 -0
- package/dist/applyParticleForces.js +129 -0
- package/dist/applyParticleForces.js.map +1 -0
- package/dist/curve.d.ts +35 -0
- package/dist/curve.d.ts.map +1 -0
- package/dist/curve.js +227 -0
- package/dist/curve.js.map +1 -0
- package/dist/emitParticleBurst.d.ts +15 -0
- package/dist/emitParticleBurst.d.ts.map +1 -0
- package/dist/emitParticleBurst.js +114 -0
- package/dist/emitParticleBurst.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/particleEmitterConfig.d.ts +4 -0
- package/dist/particleEmitterConfig.d.ts.map +1 -0
- package/dist/particleEmitterConfig.js +53 -0
- package/dist/particleEmitterConfig.js.map +1 -0
- package/dist/particleEmitterSignals.d.ts +20 -0
- package/dist/particleEmitterSignals.d.ts.map +1 -0
- package/dist/particleEmitterSignals.js +33 -0
- package/dist/particleEmitterSignals.js.map +1 -0
- package/dist/particleEmitterState.d.ts +7 -0
- package/dist/particleEmitterState.d.ts.map +1 -0
- package/dist/particleEmitterState.js +38 -0
- package/dist/particleEmitterState.js.map +1 -0
- package/dist/particleObjectsState.d.ts +4 -0
- package/dist/particleObjectsState.d.ts.map +1 -0
- package/dist/particleObjectsState.js +24 -0
- package/dist/particleObjectsState.js.map +1 -0
- package/dist/prewarmParticleEmitter.d.ts +5 -0
- package/dist/prewarmParticleEmitter.d.ts.map +1 -0
- package/dist/prewarmParticleEmitter.js +13 -0
- package/dist/prewarmParticleEmitter.js.map +1 -0
- package/dist/stepParticleEmitter.d.ts +38 -0
- package/dist/stepParticleEmitter.d.ts.map +1 -0
- package/dist/stepParticleEmitter.js +57 -0
- package/dist/stepParticleEmitter.js.map +1 -0
- package/dist/updateParticleEmitter.d.ts +8 -0
- package/dist/updateParticleEmitter.d.ts.map +1 -0
- package/dist/updateParticleEmitter.js +315 -0
- package/dist/updateParticleEmitter.js.map +1 -0
- package/dist/updateParticleObjects.d.ts +8 -0
- package/dist/updateParticleObjects.d.ts.map +1 -0
- package/dist/updateParticleObjects.js +151 -0
- package/dist/updateParticleObjects.js.map +1 -0
- package/dist/validateParticleEmitterConfig.d.ts +15 -0
- package/dist/validateParticleEmitterConfig.d.ts.map +1 -0
- package/dist/validateParticleEmitterConfig.js +206 -0
- package/dist/validateParticleEmitterConfig.js.map +1 -0
- package/package.json +42 -0
- package/src/applyParticleCollisions.test.ts +191 -0
- package/src/applyParticleForces.test.ts +174 -0
- package/src/curve.test.ts +327 -0
- package/src/deterministic.test.ts +54 -0
- package/src/emitParticleBurst.test.ts +103 -0
- package/src/particleEmitterConfig.test.ts +65 -0
- package/src/particleEmitterSignals.test.ts +125 -0
- package/src/particleEmitterState.test.ts +113 -0
- package/src/particleObjectsState.test.ts +34 -0
- package/src/prewarmParticleEmitter.test.ts +68 -0
- package/src/stepParticleEmitter.test.ts +113 -0
- package/src/updateParticleEmitter.test.ts +631 -0
- package/src/updateParticleObjects.test.ts +340 -0
- package/src/validateParticleEmitterConfig.test.ts +118 -0
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
}
|
|
36
|
+
|
|
37
|
+
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
|
+
it('returns empty arrays, zero accumulator, burstTimer=0, and NaN prev position', () => {
|
|
47
|
+
const state = createParticleEmitterState();
|
|
48
|
+
expect(state.lifetimes).toBeInstanceOf(Float32Array);
|
|
49
|
+
expect(state.lifetimes.length).toBe(0);
|
|
50
|
+
expect(state.velocities).toBeInstanceOf(Float32Array);
|
|
51
|
+
expect(state.velocities.length).toBe(0);
|
|
52
|
+
expect(state.scales).toBeInstanceOf(Float32Array);
|
|
53
|
+
expect(state.scales.length).toBe(0);
|
|
54
|
+
expect(state.rotationSpeeds).toBeInstanceOf(Float32Array);
|
|
55
|
+
expect(state.rotationSpeeds.length).toBe(0);
|
|
56
|
+
expect(state.colorBirth).toBeInstanceOf(Float32Array);
|
|
57
|
+
expect(state.colorDeath).toBeInstanceOf(Float32Array);
|
|
58
|
+
expect(state.spawnAccumulator).toBe(0);
|
|
59
|
+
expect(state.burstTimer).toBe(0);
|
|
60
|
+
expect(isNaN(state.prevX)).toBe(true);
|
|
61
|
+
expect(isNaN(state.prevY)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns a new object each call', () => {
|
|
65
|
+
const a = createParticleEmitterState();
|
|
66
|
+
const b = createParticleEmitterState();
|
|
67
|
+
expect(a).not.toBe(b);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('ensureParticleEmitterStateCapacity', () => {
|
|
72
|
+
it('grows the per-particle arrays to hold at least `capacity` particles', () => {
|
|
73
|
+
const state = createParticleEmitterState();
|
|
74
|
+
ensureParticleEmitterStateCapacity(state, 10, false);
|
|
75
|
+
expect(state.lifetimes.length).toBeGreaterThanOrEqual(10 * 2);
|
|
76
|
+
expect(state.velocities.length).toBeGreaterThanOrEqual(10 * 2);
|
|
77
|
+
expect(state.scales.length).toBeGreaterThanOrEqual(10);
|
|
78
|
+
expect(state.rotationSpeeds.length).toBeGreaterThanOrEqual(10);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('leaves the color arrays empty when the emitter has no color variance', () => {
|
|
82
|
+
const state = createParticleEmitterState();
|
|
83
|
+
ensureParticleEmitterStateCapacity(state, 8, false);
|
|
84
|
+
expect(state.colorBirth.length).toBe(0);
|
|
85
|
+
expect(state.colorDeath.length).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('grows the color arrays only when color variance is requested', () => {
|
|
89
|
+
const state = createParticleEmitterState();
|
|
90
|
+
ensureParticleEmitterStateCapacity(state, 8, true);
|
|
91
|
+
expect(state.colorBirth.length).toBeGreaterThanOrEqual(8 * 3);
|
|
92
|
+
expect(state.colorDeath.length).toBeGreaterThanOrEqual(8 * 3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('is a no-op when already large enough', () => {
|
|
96
|
+
const state = createParticleEmitterState();
|
|
97
|
+
ensureParticleEmitterStateCapacity(state, 16, false);
|
|
98
|
+
const lifetimes = state.lifetimes;
|
|
99
|
+
const velocities = state.velocities;
|
|
100
|
+
ensureParticleEmitterStateCapacity(state, 4, false); // smaller request → no reallocation
|
|
101
|
+
expect(state.lifetimes).toBe(lifetimes);
|
|
102
|
+
expect(state.velocities).toBe(velocities);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('back-fills color arrays for an already-sized state when variance turns on', () => {
|
|
106
|
+
const state = createParticleEmitterState();
|
|
107
|
+
ensureParticleEmitterStateCapacity(state, 8, false); // sized, no color
|
|
108
|
+
expect(state.colorBirth.length).toBe(0);
|
|
109
|
+
ensureParticleEmitterStateCapacity(state, 8, true); // same capacity, now wants color
|
|
110
|
+
expect(state.colorBirth.length).toBeGreaterThanOrEqual(8 * 3);
|
|
111
|
+
expect(state.colorDeath.length).toBeGreaterThanOrEqual(8 * 3);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createParticleObjectsState, ensureParticleObjectsStateCapacity } from './particleObjectsState';
|
|
2
|
+
|
|
3
|
+
describe('createParticleObjectsState', () => {
|
|
4
|
+
it('allocates arrays sized to capacity', () => {
|
|
5
|
+
const state = createParticleObjectsState(10);
|
|
6
|
+
expect(state.lifetimes.length).toBe(20);
|
|
7
|
+
expect(state.velocities.length).toBe(20);
|
|
8
|
+
expect(state.spawnAccumulator).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('all lifetime slots start as dead (maxAge = 0)', () => {
|
|
12
|
+
const state = createParticleObjectsState(5);
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
expect(state.lifetimes[i * 2 + 1]).toBe(0);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('ensureParticleObjectsStateCapacity', () => {
|
|
20
|
+
it('does not reallocate when already large enough', () => {
|
|
21
|
+
const state = createParticleObjectsState(10);
|
|
22
|
+
const { lifetimes, velocities } = state;
|
|
23
|
+
ensureParticleObjectsStateCapacity(state, 10);
|
|
24
|
+
expect(state.lifetimes).toBe(lifetimes);
|
|
25
|
+
expect(state.velocities).toBe(velocities);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('grows arrays when capacity is insufficient', () => {
|
|
29
|
+
const state = createParticleObjectsState(5);
|
|
30
|
+
ensureParticleObjectsStateCapacity(state, 20);
|
|
31
|
+
expect(state.lifetimes.length).toBeGreaterThanOrEqual(40);
|
|
32
|
+
expect(state.velocities.length).toBeGreaterThanOrEqual(40);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createParticleEmitter } from '@flighthq/sprite';
|
|
2
|
+
import type { TextureAtlas } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createParticleEmitterConfig } from './particleEmitterConfig';
|
|
5
|
+
import { createParticleEmitterState } from './particleEmitterState';
|
|
6
|
+
import { prewarmParticleEmitter } from './prewarmParticleEmitter';
|
|
7
|
+
|
|
8
|
+
function makeAtlas(): TextureAtlas {
|
|
9
|
+
return {
|
|
10
|
+
image: null,
|
|
11
|
+
regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
|
|
12
|
+
} as TextureAtlas;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('prewarmParticleEmitter', () => {
|
|
16
|
+
it('produces live particles after warming for one lifetime', () => {
|
|
17
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
18
|
+
const state = createParticleEmitterState();
|
|
19
|
+
const config = createParticleEmitterConfig({ spawnRate: 20, lifetimeMin: 1, lifetimeMax: 1 });
|
|
20
|
+
prewarmParticleEmitter(emitter, state, config, 1);
|
|
21
|
+
expect(emitter.data.particleCount).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not exceed maxParticles', () => {
|
|
25
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
26
|
+
const state = createParticleEmitterState();
|
|
27
|
+
const config = createParticleEmitterConfig({ spawnRate: 1000, maxParticles: 10, lifetimeMin: 10, lifetimeMax: 10 });
|
|
28
|
+
prewarmParticleEmitter(emitter, state, config, 5);
|
|
29
|
+
expect(emitter.data.particleCount).toBeLessThanOrEqual(10);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('accepts a custom stepDeltaTime and still produces particles', () => {
|
|
33
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
34
|
+
const state = createParticleEmitterState();
|
|
35
|
+
const config = createParticleEmitterConfig({ spawnRate: 10, lifetimeMin: 5, lifetimeMax: 5 });
|
|
36
|
+
prewarmParticleEmitter(emitter, state, config, 1, 1 / 30);
|
|
37
|
+
expect(emitter.data.particleCount).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('fires onSpawn callback during warm-up', () => {
|
|
41
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
42
|
+
const state = createParticleEmitterState();
|
|
43
|
+
const config = createParticleEmitterConfig({ spawnRate: 5, lifetimeMin: 10, lifetimeMax: 10 });
|
|
44
|
+
let spawned = 0;
|
|
45
|
+
prewarmParticleEmitter(emitter, state, config, 1, 1 / 60, {
|
|
46
|
+
onSpawn: () => {
|
|
47
|
+
spawned++;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
expect(spawned).toBeGreaterThan(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('leaves emitter unmodified when duration is zero', () => {
|
|
54
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
55
|
+
const state = createParticleEmitterState();
|
|
56
|
+
const config = createParticleEmitterConfig({ spawnRate: 100, lifetimeMin: 1, lifetimeMax: 1 });
|
|
57
|
+
prewarmParticleEmitter(emitter, state, config, 0);
|
|
58
|
+
expect(emitter.data.particleCount).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not hang when stepDeltaTime is zero (falls back to a single step)', () => {
|
|
62
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
63
|
+
const state = createParticleEmitterState();
|
|
64
|
+
const config = createParticleEmitterConfig({ spawnRate: 10, lifetimeMin: 10, lifetimeMax: 10 });
|
|
65
|
+
prewarmParticleEmitter(emitter, state, config, 1, 0);
|
|
66
|
+
expect(emitter.data.particleCount).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createParticleEmitter } from '@flighthq/sprite';
|
|
2
|
+
import type { ParticleCollider, ParticleForce, ParticleObject, TextureAtlas } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createParticleEmitterConfig } from './particleEmitterConfig';
|
|
5
|
+
import { createParticleEmitterState } from './particleEmitterState';
|
|
6
|
+
import { createParticleObjectsState } from './particleObjectsState';
|
|
7
|
+
import { stepParticleEmitter, stepParticleObjects } from './stepParticleEmitter';
|
|
8
|
+
|
|
9
|
+
function makeAtlas(): TextureAtlas {
|
|
10
|
+
return {
|
|
11
|
+
image: null,
|
|
12
|
+
regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null }],
|
|
13
|
+
} as unknown as TextureAtlas;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('stepParticleEmitter', () => {
|
|
17
|
+
it('spawns particles when called with no forces or colliders', () => {
|
|
18
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
19
|
+
const state = createParticleEmitterState();
|
|
20
|
+
const config = createParticleEmitterConfig({ spawnRate: 10, maxParticles: 100 });
|
|
21
|
+
stepParticleEmitter(emitter, state, config, 1);
|
|
22
|
+
expect(emitter.data.particleCount).toBe(10);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('applies wind force before update — particle receives velocity from wind', () => {
|
|
26
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
27
|
+
const state = createParticleEmitterState();
|
|
28
|
+
const config = createParticleEmitterConfig({
|
|
29
|
+
spawnRate: 1,
|
|
30
|
+
lifetimeMin: 10,
|
|
31
|
+
lifetimeMax: 10,
|
|
32
|
+
speedMin: 0,
|
|
33
|
+
speedMax: 0,
|
|
34
|
+
spread: 0,
|
|
35
|
+
directionX: 0,
|
|
36
|
+
directionY: -1,
|
|
37
|
+
});
|
|
38
|
+
// Spawn one particle first
|
|
39
|
+
stepParticleEmitter(emitter, state, config, 1);
|
|
40
|
+
expect(emitter.data.particleCount).toBe(1);
|
|
41
|
+
// Now step with a wind force pushing right
|
|
42
|
+
const wind: ParticleForce = { kind: 'WindForce', x: 200, y: 0 };
|
|
43
|
+
stepParticleEmitter(emitter, state, config, 1, [wind]);
|
|
44
|
+
// After wind: vx starts at 0, gets +200 from wind in applyForces, then particle moves right
|
|
45
|
+
const x = emitter.data.transforms[0];
|
|
46
|
+
expect(x).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('resolves collisions after update — particle stays above a plane at y=0', () => {
|
|
50
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
51
|
+
const state = createParticleEmitterState();
|
|
52
|
+
const config = createParticleEmitterConfig({
|
|
53
|
+
spawnRate: 1,
|
|
54
|
+
lifetimeMin: 10,
|
|
55
|
+
lifetimeMax: 10,
|
|
56
|
+
speedMin: 100,
|
|
57
|
+
speedMax: 100,
|
|
58
|
+
spread: 0,
|
|
59
|
+
directionX: 0,
|
|
60
|
+
directionY: 1, // moving downward
|
|
61
|
+
gravityY: 0,
|
|
62
|
+
});
|
|
63
|
+
stepParticleEmitter(emitter, state, config, 1);
|
|
64
|
+
// Particle moves down; plane at y=0 (normal pointing up = (0,-1), distance 0)
|
|
65
|
+
const plane: ParticleCollider = { kind: 'PlaneCollider', nx: 0, ny: -1, distance: 0, restitution: 1, friction: 0 };
|
|
66
|
+
stepParticleEmitter(emitter, state, config, 1, undefined, [plane]);
|
|
67
|
+
// Particle should be reflected above y=0
|
|
68
|
+
const y = emitter.data.transforms[1];
|
|
69
|
+
expect(y).toBeGreaterThanOrEqual(-1e-4); // at or above plane
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('passes callbacks down to updateParticleEmitter', () => {
|
|
73
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
74
|
+
const state = createParticleEmitterState();
|
|
75
|
+
const config = createParticleEmitterConfig({ spawnRate: 3, lifetimeMin: 10, lifetimeMax: 10 });
|
|
76
|
+
let spawnCount = 0;
|
|
77
|
+
stepParticleEmitter(emitter, state, config, 1, undefined, undefined, { onSpawn: () => spawnCount++ });
|
|
78
|
+
expect(spawnCount).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('is a no-op for zero deltaTime', () => {
|
|
82
|
+
const emitter = createParticleEmitter({ data: { atlas: makeAtlas() } });
|
|
83
|
+
const state = createParticleEmitterState();
|
|
84
|
+
const config = createParticleEmitterConfig({ spawnRate: 10, lifetimeMin: 10, lifetimeMax: 10 });
|
|
85
|
+
stepParticleEmitter(emitter, state, config, 0);
|
|
86
|
+
expect(emitter.data.particleCount).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('stepParticleObjects', () => {
|
|
91
|
+
it('spawns into dead slots when called with no forces or colliders', () => {
|
|
92
|
+
const objects = Array.from({ length: 10 }, makeObject);
|
|
93
|
+
const state = createParticleObjectsState(10);
|
|
94
|
+
const config = createParticleEmitterConfig({ spawnRate: 5, lifetimeMin: 10, lifetimeMax: 10 });
|
|
95
|
+
stepParticleObjects(objects, state, config, 1);
|
|
96
|
+
const visible = objects.filter((o) => o.visible).length;
|
|
97
|
+
expect(visible).toBe(5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function makeObject(): ParticleObject {
|
|
102
|
+
return {
|
|
103
|
+
alpha: 1,
|
|
104
|
+
blendMode: null,
|
|
105
|
+
colorTransform: null,
|
|
106
|
+
rotation: 0,
|
|
107
|
+
scaleX: 1,
|
|
108
|
+
scaleY: 1,
|
|
109
|
+
visible: false,
|
|
110
|
+
x: 0,
|
|
111
|
+
y: 0,
|
|
112
|
+
} as unknown as ParticleObject;
|
|
113
|
+
}
|