@lagless/pixi-react 0.0.33

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/README.md ADDED
@@ -0,0 +1,512 @@
1
+ # @lagless/pixi-react
2
+
3
+ ## 1. Responsibility & Context
4
+
5
+ Provides React integration components for Pixi.js-based game UIs: `VirtualJoystick` for touch/mouse input and `useVFXContainer` hook for Neutrino particle effects. These are UI-layer components — not part of the deterministic ECS simulation — used for rendering game controls and visual effects in the Circle Sumo frontend. Depends on `@pixi/react` for React+Pixi integration and `neutrinoparticles.pixi` for particle effects.
6
+
7
+ ## 2. Architecture Role
8
+
9
+ **UI layer** — sits on top of Pixi.js and React, provides game-specific UI components for frontend.
10
+
11
+ **Downstream consumers:**
12
+ - `circle-sumo-game` — Uses `VirtualJoystick` for player input and `useVFXContainer` for impact/collision effects
13
+
14
+ **Upstream dependencies:**
15
+ - `pixi.js` (peer dependency) — Rendering engine
16
+ - `@pixi/react` (peer dependency) — React integration for Pixi.js
17
+ - `neutrinoparticles.pixi` (peer dependency) — Particle effects library
18
+ - `@lagless/binary` — `toFloat32()` for deterministic float conversion in joystick
19
+ - `@lagless/core` — Type imports (not runtime dependency)
20
+
21
+ ## 3. Public API
22
+
23
+ ### VirtualJoystick
24
+
25
+ Touch/mouse joystick component for game input. Renders as a circular joystick UI with draggable handle.
26
+
27
+ #### VirtualJoystickProvider
28
+
29
+ React context provider that renders the joystick and manages its state.
30
+
31
+ ```typescript
32
+ const VirtualJoystickProvider: FC<{ children: ReactNode }>;
33
+ ```
34
+
35
+ **Usage:**
36
+ ```tsx
37
+ import { VirtualJoystickProvider } from '@lagless/pixi-react';
38
+
39
+ function GameApp() {
40
+ return (
41
+ <VirtualJoystickProvider>
42
+ {/* Your Pixi.js game components */}
43
+ </VirtualJoystickProvider>
44
+ );
45
+ }
46
+ ```
47
+
48
+ **What it does:**
49
+ - Loads joystick textures (`joystick.png`, `joystick-handle.png`)
50
+ - Creates `VirtualJoystickCtx` instance
51
+ - Renders joystick UI at bottom-center of canvas
52
+ - Provides joystick context via React Context
53
+
54
+ #### useVirtualJoystick
55
+
56
+ Hook to access joystick state from any component within `VirtualJoystickProvider`.
57
+
58
+ ```typescript
59
+ function useVirtualJoystick(): VirtualJoystickCtx;
60
+ ```
61
+
62
+ **Returns:** `VirtualJoystickCtx` instance with joystick state.
63
+
64
+ #### VirtualJoystickCtx
65
+
66
+ Joystick state manager. Provides real-time input values and change listeners.
67
+
68
+ ```typescript
69
+ class VirtualJoystickCtx {
70
+ get direction(): number; // Angle in radians (-π to π, 0 = right)
71
+ get axisX(): number; // Horizontal axis [-1, 1] (left to right)
72
+ get axisY(): number; // Vertical axis [-1, 1] (down to up)
73
+ get power(): number; // Distance from center [0, 1]
74
+
75
+ addDirectionChangeListener(handler: (direction: number) => void): () => void;
76
+ }
77
+
78
+ type VJDirectionListener = (direction: number) => void;
79
+ type VJUnsubscribe = () => void;
80
+ ```
81
+
82
+ **Key behavior:**
83
+ - `axisX`, `axisY`: Normalized to [-1, 1], clamped to joystick radius
84
+ - `power`: Distance from center [0, 1], 0 = center, 1 = edge
85
+ - `direction`: Angle in radians using `Math.atan2(axisY, axisX)`
86
+ - All values use `toFloat32()` for deterministic precision
87
+ - Direction listeners fire on every joystick update
88
+
89
+ #### loadVirtualJoystickAssets
90
+
91
+ Preloads joystick textures. Called automatically by `VirtualJoystickProvider`, but can be called manually for preloading.
92
+
93
+ ```typescript
94
+ function loadVirtualJoystickAssets(): Promise<VirtualJoystickAssets>;
95
+
96
+ interface VirtualJoystickAssets {
97
+ joystick: Texture; // Base joystick texture
98
+ joystickHandle: Texture; // Draggable handle texture
99
+ }
100
+ ```
101
+
102
+ ### VFX (Visual Effects)
103
+
104
+ #### useVFXContainer
105
+
106
+ React hook for managing Neutrino particle effects. Handles effect spawning, lifetime management, and cleanup.
107
+
108
+ ```typescript
109
+ function useVFXContainer(): {
110
+ containerRef: React.RefObject<Container>; // Pixi Container ref to attach to scene
111
+ spawn: (
112
+ effectAlias: string,
113
+ position: [number, number, number],
114
+ options?: SpawnOptions
115
+ ) => Effect | null;
116
+ clear: () => void; // Remove all active effects
117
+ activeCount: number; // Current number of active effects
118
+ };
119
+
120
+ interface SpawnOptions {
121
+ rotation?: number; // Rotation in radians (default: 0)
122
+ scale?: number | [number, number, number]; // Uniform or per-axis scale (default: 1)
123
+ duration?: number; // Lifetime in ms (default: auto-remove when particles = 0)
124
+ onComplete?: () => void; // Called when effect is removed
125
+ }
126
+ ```
127
+
128
+ **Effect lifecycle:**
129
+ 1. Call `spawn()` → Effect is created and added to container
130
+ 2. Effect updates every frame (via `useTick`)
131
+ 3. Effect is removed when:
132
+ - `duration` expires (if specified), OR
133
+ - `getNumParticles() === 0` (if no duration specified)
134
+ 4. `onComplete` callback fires (if provided)
135
+
136
+ **Effect alias:** Must be loaded into Pixi.js Assets cache before spawning. Use `Assets.load()` with effect JSON file.
137
+
138
+ ## 4. Preconditions
139
+
140
+ - **`VirtualJoystickProvider` requires `@pixi/react` context** — Must be rendered inside `<Application>` or `<Stage>` from `@pixi/react`
141
+ - **Joystick textures must be bundled** — `virtual-joystick/textures/joystick.png` and `joystick-handle.png` must be available via module imports
142
+ - **`useVFXContainer` requires Neutrino effects to be preloaded** — Effect models must be loaded into `Assets` cache before calling `spawn()`
143
+ - **`spawn()` must be called after containerRef is attached** — Container must be added to Pixi scene before spawning effects
144
+
145
+ ## 5. Postconditions
146
+
147
+ - After `VirtualJoystickProvider` mounts, joystick UI is visible at bottom-center of canvas
148
+ - After dragging joystick, `VirtualJoystickCtx` reflects current input state
149
+ - After `spawn()` completes, effect is visible in the container
150
+ - After `clear()` or component unmount, all effects are destroyed and removed from scene
151
+
152
+ ## 6. Invariants & Constraints
153
+
154
+ - **Joystick axis clamping:** `axisX` and `axisY` are always in [-1, 1]
155
+ - **Joystick power range:** `power` is always in [0, 1]
156
+ - **Joystick direction range:** `direction` is in (-π, π], 0 = pointing right
157
+ - **Float32 precision:** All joystick values use `toFloat32()` for deterministic rounding
158
+ - **Effect auto-removal:** Effects with no `duration` are removed when `getNumParticles() === 0`
159
+ - **Effect memory management:** All effects are destroyed on unmount to prevent memory leaks
160
+
161
+ ## 7. Safety Notes (AI Agent)
162
+
163
+ ### DO NOT
164
+
165
+ - **DO NOT use `VirtualJoystick` inside ECS systems** — This is a UI component for rendering, not game logic. Read joystick state via `useVirtualJoystick()` in React components, then send inputs to ECS via input provider.
166
+ - **DO NOT spawn effects without preloading** — `Assets.load()` must complete before calling `spawn()`, or it returns null and logs an error
167
+ - **DO NOT forget to attach containerRef** — `useVFXContainer().containerRef` must be attached to a Pixi Container in the scene, or effects won't render
168
+ - **DO NOT call `spawn()` after component unmounts** — The hook checks `isUnmountedRef` and returns null, but avoid calling spawn in async callbacks after unmount
169
+ - **DO NOT mutate `VirtualJoystickCtx` state directly** — Use `setAxis`, `setPower`, `setDirection` methods (but these are internal — typically only the joystick component calls them)
170
+
171
+ ### Common Mistakes
172
+
173
+ - Forgetting to wrap app in `VirtualJoystickProvider` → `useVirtualJoystick()` throws error
174
+ - Not attaching `containerRef` to scene → effects are created but not visible
175
+ - Spawning effects before assets load → `spawn()` returns null, no effect appears
176
+ - Not cleaning up direction listeners → memory leak if listeners are added in render loop
177
+
178
+ ## 8. Usage Examples
179
+
180
+ ### Basic VirtualJoystick Setup
181
+
182
+ ```tsx
183
+ import { Application } from '@pixi/react';
184
+ import { VirtualJoystickProvider, useVirtualJoystick } from '@lagless/pixi-react';
185
+
186
+ function Game() {
187
+ return (
188
+ <Application width={800} height={600}>
189
+ <VirtualJoystickProvider>
190
+ <PlayerController />
191
+ </VirtualJoystickProvider>
192
+ </Application>
193
+ );
194
+ }
195
+
196
+ function PlayerController() {
197
+ const joystick = useVirtualJoystick();
198
+
199
+ useEffect(() => {
200
+ // Subscribe to direction changes
201
+ const unsubscribe = joystick.addDirectionChangeListener((direction) => {
202
+ console.log(`Joystick direction: ${direction} radians`);
203
+ });
204
+
205
+ return unsubscribe; // Cleanup on unmount
206
+ }, [joystick]);
207
+
208
+ // Read joystick state
209
+ console.log(`Power: ${joystick.power}`);
210
+ console.log(`Axis: (${joystick.axisX}, ${joystick.axisY})`);
211
+
212
+ return null;
213
+ }
214
+ ```
215
+
216
+ ### Sending Joystick Input to ECS
217
+
218
+ ```tsx
219
+ import { useVirtualJoystick } from '@lagless/pixi-react';
220
+ import { useECSRunner } from './hooks';
221
+
222
+ function PlayerInputSystem() {
223
+ const joystick = useVirtualJoystick();
224
+ const runner = useECSRunner();
225
+
226
+ useEffect(() => {
227
+ const interval = setInterval(() => {
228
+ // Send input to ECS every 16ms (60 FPS)
229
+ if (joystick.power > 0.1) { // Deadzone
230
+ runner.InputProvider.sendMoveInput({
231
+ direction: joystick.direction,
232
+ power: joystick.power,
233
+ });
234
+ }
235
+ }, 16);
236
+
237
+ return () => clearInterval(interval);
238
+ }, [joystick, runner]);
239
+
240
+ return null;
241
+ }
242
+ ```
243
+
244
+ ### VFX Container Setup
245
+
246
+ ```tsx
247
+ import { Container } from '@pixi/react';
248
+ import { useVFXContainer } from '@lagless/pixi-react';
249
+ import { Assets } from 'pixi.js';
250
+ import { useEffect } from 'react';
251
+
252
+ function GameScene() {
253
+ const vfx = useVFXContainer();
254
+
255
+ useEffect(() => {
256
+ // Preload VFX assets
257
+ Assets.load('/effects/explosion.json').then(() => {
258
+ console.log('VFX loaded');
259
+ });
260
+ }, []);
261
+
262
+ const spawnExplosion = (x: number, y: number) => {
263
+ vfx.spawn('explosion', [x, y, 0], {
264
+ duration: 2000, // Remove after 2 seconds
265
+ scale: 1.5, // 1.5x scale
266
+ onComplete: () => {
267
+ console.log('Explosion complete');
268
+ },
269
+ });
270
+ };
271
+
272
+ return (
273
+ <>
274
+ {/* Attach VFX container to scene */}
275
+ <container ref={vfx.containerRef} />
276
+
277
+ {/* Game objects */}
278
+ <sprite
279
+ texture={playerTexture}
280
+ onClick={() => spawnExplosion(100, 100)}
281
+ />
282
+ </>
283
+ );
284
+ }
285
+ ```
286
+
287
+ ### Spawning VFX on Collision
288
+
289
+ ```tsx
290
+ import { useVFXContainer } from '@lagless/pixi-react';
291
+ import { useEffect } from 'react';
292
+
293
+ function CollisionEffects({ simulation }) {
294
+ const vfx = useVFXContainer();
295
+
296
+ useEffect(() => {
297
+ // Subscribe to collision signal from ECS
298
+ const unsubscribe = simulation.signals.collision.Predicted.on((event) => {
299
+ const { x, y } = event.data.position;
300
+
301
+ // Spawn impact effect
302
+ vfx.spawn('impact', [x, y, 0], {
303
+ scale: event.data.impactForce / 100, // Scale by force
304
+ rotation: event.data.angle,
305
+ // Auto-remove when particles = 0 (no duration specified)
306
+ });
307
+ });
308
+
309
+ return unsubscribe;
310
+ }, [simulation, vfx]);
311
+
312
+ return null;
313
+ }
314
+ ```
315
+
316
+ ### Clearing All Effects
317
+
318
+ ```tsx
319
+ import { useVFXContainer } from '@lagless/pixi-react';
320
+
321
+ function VFXControls() {
322
+ const vfx = useVFXContainer();
323
+
324
+ return (
325
+ <button onClick={() => vfx.clear()}>
326
+ Clear All VFX ({vfx.activeCount} active)
327
+ </button>
328
+ );
329
+ }
330
+ ```
331
+
332
+ ## 9. Testing Guidance
333
+
334
+ No tests currently exist for this library. When adding tests, consider:
335
+
336
+ **Framework suggestion:** Vitest + React Testing Library + `@testing-library/react` with Pixi.js mocking
337
+
338
+ **Test coverage priorities:**
339
+ 1. **VirtualJoystick state** — Verify `axisX`, `axisY`, `power`, `direction` update correctly on drag
340
+ 2. **VirtualJoystick clamping** — Verify values stay within valid ranges
341
+ 3. **VFX spawning** — Verify effects are added to container
342
+ 4. **VFX lifetime** — Verify effects are removed after duration or when particles = 0
343
+ 5. **Cleanup** — Verify effects are destroyed on unmount
344
+
345
+ **Challenge:** Pixi.js and `@pixi/react` are difficult to test in jsdom. Consider:
346
+ - Mocking Pixi.js classes (`Container`, `Sprite`, `Texture`)
347
+ - Using Playwright for E2E tests of actual rendered joystick
348
+
349
+ ## 10. Change Checklist
350
+
351
+ When modifying this module:
352
+
353
+ 1. **Test on touch devices** — Joystick should work identically on mobile and desktop
354
+ 2. **Check joystick positioning** — Verify joystick appears correctly on different screen sizes
355
+ 3. **Profile VFX performance** — Ensure `useTick` loop doesn't cause frame drops with many effects
356
+ 4. **Update texture assets** — If changing joystick appearance, update textures in `textures/` directory
357
+ 5. **Update this README:** Document new APIs or options
358
+ 6. **Verify cleanup** — Ensure effects are destroyed on unmount (no memory leaks)
359
+
360
+ ## 11. Integration Notes
361
+
362
+ ### Used By
363
+
364
+ - **`circle-sumo-game`:**
365
+ - `VirtualJoystick` — Player movement input on mobile/desktop
366
+ - `useVFXContainer` — Impact effects, collision effects, game-over effects
367
+
368
+ ### Common Integration Patterns
369
+
370
+ **Full game setup:**
371
+ ```tsx
372
+ import { Application, Container } from '@pixi/react';
373
+ import { VirtualJoystickProvider, useVirtualJoystick, useVFXContainer } from '@lagless/pixi-react';
374
+
375
+ function CircleSumoGame() {
376
+ return (
377
+ <Application width={1920} height={1080}>
378
+ <VirtualJoystickProvider>
379
+ <GameScene />
380
+ </VirtualJoystickProvider>
381
+ </Application>
382
+ );
383
+ }
384
+
385
+ function GameScene() {
386
+ const joystick = useVirtualJoystick();
387
+ const vfx = useVFXContainer();
388
+
389
+ // Use joystick for input
390
+ useEffect(() => {
391
+ // Send inputs to ECS
392
+ }, [joystick]);
393
+
394
+ // Subscribe to game signals for VFX
395
+ useEffect(() => {
396
+ // Spawn effects on collisions
397
+ }, [vfx]);
398
+
399
+ return (
400
+ <>
401
+ <container ref={vfx.containerRef} />
402
+ {/* Game objects */}
403
+ </>
404
+ );
405
+ }
406
+ ```
407
+
408
+ **Preloading VFX assets:**
409
+ ```tsx
410
+ import { Assets, EffectModel } from 'pixi.js';
411
+
412
+ async function preloadGameAssets() {
413
+ await Assets.load([
414
+ '/effects/explosion.json',
415
+ '/effects/impact.json',
416
+ '/effects/smoke.json',
417
+ ]);
418
+
419
+ // Verify effects loaded
420
+ const explosion = Assets.get<EffectModel>('explosion');
421
+ console.log('Explosion effect loaded:', explosion !== null);
422
+ }
423
+ ```
424
+
425
+ ## 12. Appendix
426
+
427
+ ### VirtualJoystick Coordinate System
428
+
429
+ ```
430
+ Y-axis points UP (positive Y = up)
431
+ X-axis points RIGHT (positive X = right)
432
+
433
+ axisY = +1
434
+
435
+ |
436
+ axisX = -1 ← O → axisX = +1
437
+ |
438
+
439
+ axisY = -1
440
+
441
+ Direction (radians):
442
+ π/2 (up)
443
+ |
444
+ π ←─── O ───→ 0 (right)
445
+ |
446
+ -π/2 (down)
447
+ ```
448
+
449
+ **Conversion to game input:**
450
+ ```typescript
451
+ // Joystick uses Y-up convention
452
+ // Game may use Y-down (Pixi.js default)
453
+ const gameX = joystick.axisX;
454
+ const gameY = -joystick.axisY; // Flip Y axis if game uses Y-down
455
+ ```
456
+
457
+ ### Joystick Positioning
458
+
459
+ Joystick is rendered at:
460
+ ```
461
+ x = canvasWidth / 2 - joystickSize / 2 // Centered horizontally
462
+ y = canvasHeight - joystickSize - canvasHeight * 0.1 // 10% padding from bottom
463
+ ```
464
+
465
+ **To customize position**, modify `VirtualJoystick` component (lines 156-158).
466
+
467
+ ### VFX Effect Lifecycle
468
+
469
+ ```
470
+ 1. spawn() called
471
+
472
+ 2. Effect created and added to container
473
+
474
+ 3. useTick() updates effect every frame
475
+
476
+ 4. Check removal conditions:
477
+ - duration expired? → Remove
478
+ - getNumParticles() === 0 && no duration? → Remove
479
+
480
+ 5. effect.destroy() + onComplete() called
481
+ ```
482
+
483
+ **Duration vs Auto-removal:**
484
+ - **With duration:** Effect removed after `duration` ms, even if particles still exist
485
+ - **Without duration:** Effect removed when `getNumParticles() === 0` (all particles dead)
486
+
487
+ ### Neutrino Particles Asset Format
488
+
489
+ Neutrino effects are JSON files with embedded texture references. Example structure:
490
+
491
+ ```json
492
+ {
493
+ "effect": {
494
+ "name": "explosion",
495
+ "emitters": [...],
496
+ "textures": [
497
+ { "id": "particle1", "url": "/textures/particle1.png" }
498
+ ]
499
+ }
500
+ }
501
+ ```
502
+
503
+ **Loading:**
504
+ ```typescript
505
+ await Assets.load('/effects/explosion.json');
506
+ const effectModel = Assets.get<EffectModel>('explosion');
507
+ ```
508
+
509
+ **Spawning:**
510
+ ```typescript
511
+ vfx.spawn('explosion', [x, y, z], options);
512
+ ```
@@ -0,0 +1,4 @@
1
+ export * from './lib/virtual-joystick/virtual-joystick';
2
+ export * from './lib/neutrino-particles/use-vfx-container';
3
+ export * from './lib/filter-views/filter-views';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yCAAyC,CAAC;AACxD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,iCAAiC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './lib/virtual-joystick/virtual-joystick';
2
+ export * from './lib/neutrino-particles/use-vfx-container';
3
+ export * from './lib/filter-views/filter-views';
@@ -0,0 +1,19 @@
1
+ import { FC, ForwardRefExoticComponent, ForwardRefRenderFunction, RefAttributes } from 'react';
2
+ import { AbstractFilter } from '@lagless/core';
3
+ export interface FilterViewRef {
4
+ onCreate(): void;
5
+ onUpdate(): void;
6
+ onDestroy(): Promise<void> | void;
7
+ }
8
+ export type FilterViewProps = {
9
+ entity: number;
10
+ };
11
+ export type FilterView = ForwardRefExoticComponent<FilterViewProps & RefAttributes<FilterViewRef>>;
12
+ export declare const filterView: (render: ForwardRefRenderFunction<FilterViewRef, FilterViewProps>) => ForwardRefExoticComponent<FilterViewProps & RefAttributes<FilterViewRef>>;
13
+ interface FilterViewsProps {
14
+ View: FilterView;
15
+ filter: AbstractFilter;
16
+ }
17
+ export declare const FilterViews: FC<FilterViewsProps>;
18
+ export {};
19
+ //# sourceMappingURL=filter-views.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-views.d.ts","sourceRoot":"","sources":["../../../src/lib/filter-views/filter-views.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,EAAE,EAEF,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EAKd,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,IAAI,IAAI,CAAC;IACjB,QAAQ,IAAI,IAAI,CAAC;IACjB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,yBAAyB,CAAC,eAAe,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC;AAEnG,eAAO,MAAM,UAAU,GAAI,QAAQ,wBAAwB,CAAC,aAAa,EAAE,eAAe,CAAC,8EAE1F,CAAC;AAEF,UAAU,gBAAgB;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAyH5C,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useRef, useState, } from 'react';
3
+ import { useTick } from '@pixi/react';
4
+ export const filterView = (render) => {
5
+ return forwardRef(render);
6
+ };
7
+ export const FilterViews = ({ filter, View }) => {
8
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
9
+ const viewsContainerRef = useRef(null);
10
+ const liveEntitiesRef = useRef(new Set());
11
+ const destroyingRef = useRef(new Set());
12
+ const entityRefs = useRef(new Map());
13
+ const destroyTokenRef = useRef(new Map());
14
+ const destroyTokenCounterRef = useRef(0);
15
+ const createdRef = useRef(new Set());
16
+ const [renderedEntities, setRenderedEntities] = useState([]);
17
+ const needsSyncRef = useRef(false);
18
+ const isMountedRef = useRef(false);
19
+ useEffect(() => {
20
+ isMountedRef.current = true;
21
+ return () => {
22
+ isMountedRef.current = false;
23
+ };
24
+ }, []);
25
+ const startDestroy = useCallback((entity) => {
26
+ const ref = entityRefs.current.get(entity);
27
+ destroyingRef.current.add(entity);
28
+ const token = ++destroyTokenCounterRef.current;
29
+ destroyTokenRef.current.set(entity, token);
30
+ const finalize = () => {
31
+ const currentToken = destroyTokenRef.current.get(entity);
32
+ if (currentToken !== token)
33
+ return;
34
+ destroyTokenRef.current.delete(entity);
35
+ destroyingRef.current.delete(entity);
36
+ liveEntitiesRef.current.delete(entity);
37
+ entityRefs.current.delete(entity);
38
+ createdRef.current.delete(entity);
39
+ if (!isMountedRef.current)
40
+ return;
41
+ needsSyncRef.current = true;
42
+ };
43
+ const maybePromise = ref?.onDestroy?.();
44
+ if (maybePromise && typeof maybePromise.then === 'function') {
45
+ maybePromise.then(finalize, finalize);
46
+ }
47
+ else {
48
+ finalize();
49
+ }
50
+ }, []);
51
+ useTick(useCallback(() => {
52
+ const live = liveEntitiesRef.current;
53
+ const destroying = destroyingRef.current;
54
+ const seen = new Set();
55
+ for (const entity of filter) {
56
+ seen.add(entity);
57
+ const wasNew = !live.has(entity);
58
+ if (wasNew) {
59
+ live.add(entity);
60
+ needsSyncRef.current = true;
61
+ }
62
+ if (destroying.has(entity)) {
63
+ destroying.delete(entity);
64
+ destroyTokenRef.current.delete(entity);
65
+ }
66
+ if (!wasNew) {
67
+ const ref = entityRefs.current.get(entity);
68
+ if (ref)
69
+ ref.onUpdate();
70
+ }
71
+ }
72
+ live.forEach((entity) => {
73
+ if (!seen.has(entity) && !destroying.has(entity)) {
74
+ startDestroy(entity);
75
+ }
76
+ });
77
+ if (needsSyncRef.current && isMountedRef.current) {
78
+ needsSyncRef.current = false;
79
+ setRenderedEntities(Array.from(liveEntitiesRef.current));
80
+ }
81
+ }, [filter, startDestroy]));
82
+ useEffect(() => {
83
+ return () => {
84
+ for (const [, ref] of entityRefs.current) {
85
+ try {
86
+ ref?.onDestroy?.();
87
+ }
88
+ catch {
89
+ // ignore
90
+ }
91
+ }
92
+ };
93
+ }, []);
94
+ return (_jsx("pixiContainer", { ref: viewsContainerRef, children: renderedEntities.map((entity) => (_jsx(View, { entity: entity, ref: (instance) => {
95
+ if (instance) {
96
+ entityRefs.current.set(entity, instance);
97
+ if (!createdRef.current.has(entity)) {
98
+ createdRef.current.add(entity);
99
+ instance.onCreate();
100
+ }
101
+ }
102
+ else {
103
+ entityRefs.current.delete(entity);
104
+ }
105
+ } }, entity))) }));
106
+ };
@@ -0,0 +1,19 @@
1
+ import { Container } from 'pixi.js';
2
+ import { Effect } from 'neutrinoparticles.pixi';
3
+ interface SpawnOptions {
4
+ rotation?: number;
5
+ scale?: number | [number, number, number];
6
+ /** Время жизни в мс. Если не указано — удалится когда частиц станет 0 */
7
+ duration?: number;
8
+ /** Callback при удалении эффекта */
9
+ onComplete?: () => void;
10
+ }
11
+ export declare const useVFXContainer: () => {
12
+ containerRef: import("react").RefObject<Container<import("pixi.js").ContainerChild> | null>;
13
+ spawn: (effectAlias: string, position: [number, number, number], options?: SpawnOptions) => Effect | null;
14
+ clear: () => void;
15
+ /** Текущее количество активных эффектов */
16
+ readonly activeCount: number;
17
+ };
18
+ export {};
19
+ //# sourceMappingURL=use-vfx-container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-vfx-container.d.ts","sourceRoot":"","sources":["../../../src/lib/neutrino-particles/use-vfx-container.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAU,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAsB,MAAM,wBAAwB,CAAC;AAGpE,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAQD,eAAO,MAAM,eAAe;;yBAsCX,MAAM,YACT,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,YACxB,YAAY,KACrB,MAAM,GAAG,IAAI;;IAmEd,2CAA2C;;CAG9C,CAAC"}