@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.
- package/dist/applyParticleCollisions.d.ts +2 -2
- package/dist/applyParticleCollisions.d.ts.map +1 -1
- package/dist/applyParticleCollisions.js +75 -30
- package/dist/applyParticleCollisions.js.map +1 -1
- package/dist/applyParticleForces.d.ts.map +1 -1
- package/dist/applyParticleForces.js +33 -10
- package/dist/applyParticleForces.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/particleEmitterConfig.d.ts.map +1 -1
- package/dist/particleEmitterConfig.js +4 -0
- package/dist/particleEmitterConfig.js.map +1 -1
- package/dist/particleEmitterState.d.ts +1 -0
- package/dist/particleEmitterState.d.ts.map +1 -1
- package/dist/particleEmitterState.js +4 -1
- package/dist/particleEmitterState.js.map +1 -1
- package/dist/stepParticleObjects.d.ts +19 -0
- package/dist/stepParticleObjects.d.ts.map +1 -0
- package/dist/stepParticleObjects.js +29 -0
- package/dist/stepParticleObjects.js.map +1 -0
- package/dist/validateParticleEmitterConfig.d.ts.map +1 -1
- package/dist/validateParticleEmitterConfig.js +8 -0
- package/dist/validateParticleEmitterConfig.js.map +1 -1
- package/package.json +10 -7
- package/src/applyParticleCollisions.test.ts +71 -32
- package/src/applyParticleForces.test.ts +70 -34
- package/src/curve.test.ts +0 -63
- package/src/particleEmitterConfig.test.ts +4 -0
- package/src/particleEmitterSignals.test.ts +0 -83
- package/src/particleEmitterState.test.ts +7 -44
- package/src/stepParticleObjects.test.ts +30 -0
- package/src/validateParticleEmitterConfig.test.ts +0 -32
- package/dist/emitParticleBurst.d.ts +0 -15
- package/dist/emitParticleBurst.d.ts.map +0 -1
- package/dist/emitParticleBurst.js +0 -114
- package/dist/emitParticleBurst.js.map +0 -1
- package/dist/prewarmParticleEmitter.d.ts +0 -5
- package/dist/prewarmParticleEmitter.d.ts.map +0 -1
- package/dist/prewarmParticleEmitter.js +0 -13
- package/dist/prewarmParticleEmitter.js.map +0 -1
- package/dist/stepParticleEmitter.d.ts +0 -38
- package/dist/stepParticleEmitter.d.ts.map +0 -1
- package/dist/stepParticleEmitter.js +0 -57
- package/dist/stepParticleEmitter.js.map +0 -1
- package/dist/updateParticleEmitter.d.ts +0 -8
- package/dist/updateParticleEmitter.d.ts.map +0 -1
- package/dist/updateParticleEmitter.js +0 -315
- package/dist/updateParticleEmitter.js.map +0 -1
- package/src/deterministic.test.ts +0 -54
- package/src/emitParticleBurst.test.ts +0 -103
- package/src/prewarmParticleEmitter.test.ts +0 -68
- package/src/stepParticleEmitter.test.ts +0 -113
- 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.
|
|
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.
|
|
31
|
-
"@flighthq/
|
|
32
|
-
"@flighthq/
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
//
|
|
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 =
|
|
27
|
+
const emitter = createEmitterFixture(1);
|
|
21
28
|
const state = createParticleEmitterState();
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
28
|
+
// A single stationary particle at the origin with zeroed velocity state.
|
|
20
29
|
function oneParticle() {
|
|
21
|
-
const emitter =
|
|
30
|
+
const emitter = createEmitterFixture(1);
|
|
22
31
|
const state = createParticleEmitterState();
|
|
23
|
-
|
|
24
|
-
|
|
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('
|
|
124
|
+
it('wind z component adds acceleration on the Z axis', () => {
|
|
122
125
|
const { emitter, state } = oneParticle();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 *
|
|
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
|
+
});
|