@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 +512 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/lib/filter-views/filter-views.d.ts +19 -0
- package/dist/lib/filter-views/filter-views.d.ts.map +1 -0
- package/dist/lib/filter-views/filter-views.js +106 -0
- package/dist/lib/neutrino-particles/use-vfx-container.d.ts +19 -0
- package/dist/lib/neutrino-particles/use-vfx-container.d.ts.map +1 -0
- package/dist/lib/neutrino-particles/use-vfx-container.js +96 -0
- package/dist/lib/virtual-joystick/virtual-joystick-ctx.d.ts +18 -0
- package/dist/lib/virtual-joystick/virtual-joystick-ctx.d.ts.map +1 -0
- package/dist/lib/virtual-joystick/virtual-joystick-ctx.js +37 -0
- package/dist/lib/virtual-joystick/virtual-joystick.d.ts +14 -0
- package/dist/lib/virtual-joystick/virtual-joystick.d.ts.map +1 -0
- package/dist/lib/virtual-joystick/virtual-joystick.js +96 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +39 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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,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"}
|