@energy8platform/game-engine 0.21.0 → 0.23.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.
Files changed (65) hide show
  1. package/dist/host.d.ts +3 -5
  2. package/dist/index.cjs.js +0 -2532
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.d.ts +3 -1020
  5. package/dist/index.esm.js +2 -2522
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/slot.cjs.js +0 -30
  8. package/dist/slot.cjs.js.map +1 -1
  9. package/dist/slot.d.ts +2 -25
  10. package/dist/slot.esm.js +1 -30
  11. package/dist/slot.esm.js.map +1 -1
  12. package/dist/vite.cjs.js +0 -5
  13. package/dist/vite.cjs.js.map +1 -1
  14. package/dist/vite.esm.js +0 -5
  15. package/dist/vite.esm.js.map +1 -1
  16. package/package.json +4 -37
  17. package/src/host/index.ts +0 -1
  18. package/src/host/types.ts +0 -3
  19. package/src/index.ts +0 -28
  20. package/src/slot/index.ts +0 -2
  21. package/src/vite/index.ts +0 -5
  22. package/dist/react-jsx.cjs.js +0 -3
  23. package/dist/react-jsx.cjs.js.map +0 -1
  24. package/dist/react-jsx.d.ts +0 -602
  25. package/dist/react-jsx.esm.js +0 -2
  26. package/dist/react-jsx.esm.js.map +0 -1
  27. package/dist/react.cjs.js +0 -3802
  28. package/dist/react.cjs.js.map +0 -1
  29. package/dist/react.d.ts +0 -1467
  30. package/dist/react.esm.js +0 -3786
  31. package/dist/react.esm.js.map +0 -1
  32. package/dist/ui.cjs.js +0 -3014
  33. package/dist/ui.cjs.js.map +0 -1
  34. package/dist/ui.d.ts +0 -1116
  35. package/dist/ui.esm.js +0 -2998
  36. package/dist/ui.esm.js.map +0 -1
  37. package/src/react/EngineContext.ts +0 -26
  38. package/src/react/ReactScene.ts +0 -88
  39. package/src/react/applyProps.ts +0 -271
  40. package/src/react/catalogue.ts +0 -17
  41. package/src/react/createPixiRoot.ts +0 -31
  42. package/src/react/extendAll.ts +0 -74
  43. package/src/react/hooks.ts +0 -46
  44. package/src/react/index.ts +0 -29
  45. package/src/react/jsx-runtime.ts +0 -346
  46. package/src/react/reconciler.ts +0 -338
  47. package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
  48. package/src/state/StateMachine.ts +0 -231
  49. package/src/state/index.ts +0 -1
  50. package/src/ui/BalanceDisplay.ts +0 -159
  51. package/src/ui/Button.ts +0 -304
  52. package/src/ui/FlexContainer.ts +0 -775
  53. package/src/ui/Label.ts +0 -124
  54. package/src/ui/LabelValue.ts +0 -122
  55. package/src/ui/Layout.ts +0 -291
  56. package/src/ui/Modal.ts +0 -170
  57. package/src/ui/Panel.ts +0 -204
  58. package/src/ui/ProgressBar.ts +0 -170
  59. package/src/ui/ScrollContainer.ts +0 -478
  60. package/src/ui/Slider.ts +0 -241
  61. package/src/ui/Toast.ts +0 -150
  62. package/src/ui/Toggle.ts +0 -201
  63. package/src/ui/WinDisplay.ts +0 -145
  64. package/src/ui/index.ts +0 -33
  65. package/src/ui/view.ts +0 -28
package/dist/react.cjs.js DELETED
@@ -1,3802 +0,0 @@
1
- 'use strict';
2
-
3
- var constants = require('react-reconciler/constants');
4
- var Reconciler = require('react-reconciler');
5
- var pixi_js = require('pixi.js');
6
- var react = require('react');
7
-
8
- /** Mutable catalogue: PascalCase name -> PixiJS constructor */
9
- const catalogue = {};
10
- /**
11
- * Register PixiJS classes for use as JSX elements.
12
- * Keys must be PascalCase; JSX uses the camelCase equivalent.
13
- *
14
- * @example
15
- * ```ts
16
- * import { Container, Sprite, Text } from 'pixi.js';
17
- * extend({ Container, Sprite, Text });
18
- * // Now <container>, <sprite>, <text> work in JSX
19
- * ```
20
- */
21
- function extend(components) {
22
- Object.assign(catalogue, components);
23
- }
24
-
25
- const RESERVED = new Set(['children', 'key', 'ref']);
26
- /** Props handled by the reconciler as flex item config, not forwarded to components */
27
- const FLEX_ITEM_PROPS$1 = new Set(['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude', 'top', 'right', 'bottom', 'left', 'centerX', 'centerY']);
28
- /** Base Container props applied directly to the instance (not via config) */
29
- const CONTAINER_PROPS = new Set([
30
- 'x', 'y', 'alpha', 'visible', 'rotation', 'angle', 'zIndex',
31
- 'label', 'cursor', 'eventMode',
32
- ]);
33
- // ─── UI Component helpers ────────────────────────────────
34
- /**
35
- * Extract a config object from React props.
36
- * - Strips reserved keys (children, key, ref) and event props
37
- * - Unfolds dash-notation into nested objects: `colors-default` → `{ colors: { default: ... } }`
38
- */
39
- function extractConfig(props) {
40
- const config = {};
41
- for (const key in props) {
42
- if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || CONTAINER_PROPS.has(key) || isEventProp(key))
43
- continue;
44
- if (key.includes('-')) {
45
- const parts = key.split('-');
46
- const root = parts[0];
47
- const nested = parts.slice(1).join('-');
48
- if (!config[root] || typeof config[root] !== 'object') {
49
- config[root] = {};
50
- }
51
- config[root][nested] = props[key];
52
- }
53
- else {
54
- config[key] = props[key];
55
- }
56
- }
57
- return config;
58
- }
59
- /**
60
- * Diff two prop sets and return a config object with only changed values.
61
- * Uses extractConfig format (dash-notation unfolded).
62
- */
63
- function diffConfig(newProps, oldProps) {
64
- const changed = {};
65
- // New or changed props
66
- for (const key in newProps) {
67
- if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || CONTAINER_PROPS.has(key) || isEventProp(key))
68
- continue;
69
- if (newProps[key] !== oldProps[key]) {
70
- if (key.includes('-')) {
71
- const parts = key.split('-');
72
- const root = parts[0];
73
- const nested = parts.slice(1).join('-');
74
- if (!changed[root] || typeof changed[root] !== 'object') {
75
- changed[root] = {};
76
- }
77
- changed[root][nested] = newProps[key];
78
- }
79
- else {
80
- changed[key] = newProps[key];
81
- }
82
- }
83
- }
84
- return changed;
85
- }
86
- /**
87
- * Apply only event props from React props to a PixiJS instance.
88
- */
89
- function applyEventProps(instance, newProps, oldProps = {}) {
90
- // Remove old event handlers
91
- for (const key in oldProps) {
92
- if (!isEventProp(key) || key in newProps)
93
- continue;
94
- instance[REACT_TO_PIXI_EVENTS[key]] = null;
95
- }
96
- // Apply new/changed event handlers + onPress (component-level callback)
97
- for (const key in newProps) {
98
- if (key === 'onPress') {
99
- instance.onPress = newProps[key];
100
- continue;
101
- }
102
- if (!isEventProp(key))
103
- continue;
104
- if (newProps[key] !== oldProps[key]) {
105
- instance[REACT_TO_PIXI_EVENTS[key]] = newProps[key];
106
- }
107
- }
108
- }
109
- const REACT_TO_PIXI_EVENTS = {
110
- onClick: 'onclick',
111
- onPointerDown: 'onpointerdown',
112
- onPointerUp: 'onpointerup',
113
- onPointerMove: 'onpointermove',
114
- onPointerOver: 'onpointerover',
115
- onPointerOut: 'onpointerout',
116
- onPointerEnter: 'onpointerenter',
117
- onPointerLeave: 'onpointerleave',
118
- onPointerCancel: 'onpointercancel',
119
- onPointerTap: 'onpointertap',
120
- onPointerUpOutside: 'onpointerupoutside',
121
- onMouseDown: 'onmousedown',
122
- onMouseUp: 'onmouseup',
123
- onMouseMove: 'onmousemove',
124
- onMouseOver: 'onmouseover',
125
- onMouseOut: 'onmouseout',
126
- onMouseEnter: 'onmouseenter',
127
- onMouseLeave: 'onmouseleave',
128
- onMouseUpOutside: 'onmouseupoutside',
129
- onTouchStart: 'ontouchstart',
130
- onTouchEnd: 'ontouchend',
131
- onTouchMove: 'ontouchmove',
132
- onTouchCancel: 'ontouchcancel',
133
- onTouchEndOutside: 'ontouchendoutside',
134
- onWheel: 'onwheel',
135
- onRightClick: 'onrightclick',
136
- onRightDown: 'onrightdown',
137
- onRightUp: 'onrightup',
138
- onRightUpOutside: 'onrightupoutside',
139
- onTap: 'ontap',
140
- onGlobalpointermove: 'onglobalpointermove',
141
- onGlobalmousemove: 'onglobalmousemove',
142
- onGlobaltouchmove: 'onglobaltouchmove',
143
- };
144
- function isEventProp(key) {
145
- return key in REACT_TO_PIXI_EVENTS;
146
- }
147
- function hasEventProps(props) {
148
- for (const key in props) {
149
- if (isEventProp(key))
150
- return true;
151
- }
152
- return false;
153
- }
154
- /**
155
- * Graphics does not have an intrinsic size — its bounds come from the `draw` callback.
156
- * PixiJS's `Container.width` setter applies a scale transform to match the requested
157
- * value, which silently makes the rendered size diverge from `getLocalBounds()` and
158
- * breaks downstream flex layout. For Graphics we treat `width`/`height` as layout
159
- * hints instead, storing them on `_flexConfig.layoutWidth`/`layoutHeight` so parent
160
- * FlexContainers measure correctly without touching the child's scale.
161
- */
162
- function isGraphicsLike(instance) {
163
- return typeof instance?.clear === 'function'
164
- && typeof instance?.fill === 'function'
165
- && typeof instance?.rect === 'function';
166
- }
167
- /** Redirect a width/height write on a Graphics into its flex layout hint. Returns true if handled. */
168
- function applyGraphicsDimension(instance, key, value) {
169
- if ((key !== 'width' && key !== 'height') || !isGraphicsLike(instance))
170
- return false;
171
- const target = key === 'width' ? 'layoutWidth' : 'layoutHeight';
172
- if (value === undefined) {
173
- if (instance._flexConfig)
174
- delete instance._flexConfig[target];
175
- return true;
176
- }
177
- if (!instance._flexConfig)
178
- instance._flexConfig = {};
179
- instance._flexConfig[target] = value;
180
- return true;
181
- }
182
- function setNestedValue(target, path, value) {
183
- let obj = target;
184
- for (let i = 0; i < path.length - 1; i++) {
185
- obj = obj[path[i]];
186
- if (obj == null)
187
- return;
188
- }
189
- obj[path[path.length - 1]] = value;
190
- }
191
- /**
192
- * Apply base Container props (x, y, alpha, visible, etc.) directly to the instance.
193
- * Called for ALL elements — both UI components and standard PixiJS elements.
194
- * Also handles scale, pivot, position, anchor via dash-notation.
195
- */
196
- function applyContainerProps(instance, newProps, oldProps = {}) {
197
- for (const key of CONTAINER_PROPS) {
198
- if (key in newProps && newProps[key] !== oldProps[key]) {
199
- try {
200
- instance[key] = newProps[key];
201
- }
202
- catch { /* read-only */ }
203
- }
204
- else if (key in oldProps && !(key in newProps)) {
205
- // Prop removed — reset to undefined (PixiJS defaults)
206
- try {
207
- instance[key] = undefined;
208
- }
209
- catch { /* read-only */ }
210
- }
211
- }
212
- // Handle scale as uniform number or {x,y} object
213
- if ('scale' in newProps && newProps.scale !== oldProps.scale) {
214
- const s = newProps.scale;
215
- if (typeof s === 'number') {
216
- instance.scale?.set?.(s, s);
217
- }
218
- else if (s && typeof s === 'object') {
219
- instance.scale?.set?.(s.x ?? 1, s.y ?? 1);
220
- }
221
- }
222
- // Handle dash-notation container props: scale-x, scale-y, pivot-x, pivot-y, position-x, position-y, anchor-x, anchor-y
223
- for (const key in newProps) {
224
- if (!key.includes('-'))
225
- continue;
226
- const parts = key.split('-');
227
- const root = parts[0];
228
- if (root === 'scale' || root === 'pivot' || root === 'position' || root === 'anchor') {
229
- if (newProps[key] !== oldProps[key]) {
230
- setNestedValue(instance, parts, newProps[key]);
231
- }
232
- }
233
- }
234
- }
235
- function applyProps(instance, newProps, oldProps = {}) {
236
- // Remove old props not in newProps
237
- for (const key in oldProps) {
238
- if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || key in newProps)
239
- continue;
240
- const pixiEvent = REACT_TO_PIXI_EVENTS[key];
241
- if (pixiEvent) {
242
- instance[pixiEvent] = null;
243
- }
244
- else if (key === 'draw') ;
245
- else if (key.includes('-')) ;
246
- else if (applyGraphicsDimension(instance, key, undefined)) ;
247
- else {
248
- try {
249
- instance[key] = undefined;
250
- }
251
- catch {
252
- // read-only or non-configurable
253
- }
254
- }
255
- }
256
- // Apply new props
257
- for (const key in newProps) {
258
- if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key))
259
- continue;
260
- const value = newProps[key];
261
- const pixiEvent = REACT_TO_PIXI_EVENTS[key];
262
- if (pixiEvent) {
263
- instance[pixiEvent] = value;
264
- }
265
- else if (key === 'draw' && typeof value === 'function') {
266
- instance.clear?.();
267
- value(instance);
268
- }
269
- else if (key.includes('-')) {
270
- const parts = key.split('-');
271
- setNestedValue(instance, parts, value);
272
- }
273
- else if (applyGraphicsDimension(instance, key, value)) ;
274
- else {
275
- try {
276
- instance[key] = value;
277
- }
278
- catch {
279
- // read-only property
280
- }
281
- }
282
- }
283
- }
284
-
285
- // ─── Helpers ─────────────────────────────────────────────
286
- function normalizePadding(p) {
287
- return typeof p === 'number' ? [p, p, p, p] : p;
288
- }
289
- /** Resolve padding from config: individual props override the base `padding` value */
290
- function resolvePadding(config) {
291
- const base = normalizePadding(config.padding ?? 0);
292
- return [
293
- config.paddingTop ?? base[0],
294
- config.paddingRight ?? base[1],
295
- config.paddingBottom ?? base[2],
296
- config.paddingLeft ?? base[3],
297
- ];
298
- }
299
- /** Resolve a dimension value — number passes through, "50%" resolves against reference */
300
- function resolveDimension(value, reference) {
301
- if (value === undefined)
302
- return undefined;
303
- if (typeof value === 'number')
304
- return value;
305
- if (typeof value === 'string' && value.endsWith('%')) {
306
- const pct = parseFloat(value);
307
- if (!isNaN(pct) && reference > 0 && isFinite(reference))
308
- return (pct / 100) * reference;
309
- }
310
- return undefined;
311
- }
312
- /** Measure a child's size and bounds offset for layout purposes */
313
- function measureChild(child, parentContentW = 0, parentContentH = 0) {
314
- const cfg = child._flexConfig;
315
- const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
316
- const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
317
- // FlexContainer children are top-left origin by construction, so `ox/oy = 0`
318
- // is correct and we can skip the `getLocalBounds()` call entirely.
319
- if (child instanceof FlexContainer) {
320
- const fc = child;
321
- const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
322
- const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
323
- if (w !== undefined && h !== undefined) {
324
- return { w: resolvedLW ?? w, h: resolvedLH ?? h, ox: 0, oy: 0 };
325
- }
326
- }
327
- // Use localBounds to get the true visual origin offset.
328
- // We take `ox/oy` from bounds unconditionally — a user-supplied `layoutWidth/Height`
329
- // overrides the SIZE used by flex (protection against pre-paint bounds of 0), but
330
- // it must NOT erase the center-anchor compensation: Button/Label views are drawn at
331
- // `(-w/2..w/2)` and their bounds.x = -w/2 is what lets `layoutLine` place the
332
- // visible rectangle where the layout intends.
333
- const bounds = child.getLocalBounds();
334
- return {
335
- w: resolvedLW ?? bounds.width,
336
- h: resolvedLH ?? bounds.height,
337
- ox: bounds.x,
338
- oy: bounds.y,
339
- };
340
- }
341
- /**
342
- * Set a child's main or cross dimension.
343
- * For FlexContainer children, calls resize() to trigger internal relayout
344
- * instead of the PixiJS scale setter.
345
- */
346
- function setChildMainSize(child, isRow, mainSize, item) {
347
- if (child instanceof FlexContainer) {
348
- const fc = child;
349
- fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
350
- }
351
- else {
352
- if (isRow) {
353
- child.width = mainSize;
354
- }
355
- else {
356
- child.height = mainSize;
357
- }
358
- }
359
- if (isRow)
360
- item.w = mainSize;
361
- else
362
- item.h = mainSize;
363
- }
364
- function setChildCrossSize(child, isRow, crossSize) {
365
- if (child instanceof FlexContainer) {
366
- const fc = child;
367
- fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
368
- }
369
- else {
370
- if (isRow) {
371
- child.height = crossSize;
372
- }
373
- else {
374
- child.width = crossSize;
375
- }
376
- }
377
- }
378
- function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
379
- if (items.length === 0)
380
- return;
381
- // Compute total fixed main size and flex grow total
382
- let totalFixed = 0;
383
- let totalGrow = 0;
384
- for (const item of items) {
385
- const grow = item.child._flexConfig?.flexGrow ?? 0;
386
- if (grow > 0) {
387
- totalGrow += grow;
388
- }
389
- else {
390
- totalFixed += isRow ? item.w : item.h;
391
- }
392
- }
393
- const totalGap = gap * (items.length - 1);
394
- const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
395
- // Resolve flex sizes
396
- if (totalGrow > 0) {
397
- for (const item of items) {
398
- const grow = item.child._flexConfig?.flexGrow ?? 0;
399
- if (grow > 0) {
400
- const flexSize = (grow / totalGrow) * availableForFlex;
401
- setChildMainSize(item.child, isRow, flexSize, item);
402
- }
403
- }
404
- }
405
- // Shrink: if content overflows and mainSize is finite, shrink eligible items
406
- if (totalGrow === 0 && mainSize > 0) {
407
- const overflow = totalFixed + totalGap - mainSize;
408
- if (overflow > 0) {
409
- let totalShrinkable = 0;
410
- for (const item of items) {
411
- const shrink = item.child._flexConfig?.flexShrink ?? 1;
412
- if (shrink > 0) {
413
- totalShrinkable += isRow ? item.w : item.h;
414
- }
415
- }
416
- if (totalShrinkable > 0) {
417
- for (const item of items) {
418
- const shrink = item.child._flexConfig?.flexShrink ?? 1;
419
- if (shrink > 0) {
420
- const itemMain = isRow ? item.w : item.h;
421
- const reduction = overflow * (itemMain / totalShrinkable);
422
- const newSize = Math.max(0, itemMain - reduction);
423
- setChildMainSize(item.child, isRow, newSize, item);
424
- }
425
- }
426
- }
427
- }
428
- }
429
- // Calculate total main size after flex
430
- let totalMain = totalGap;
431
- for (const item of items) {
432
- totalMain += isRow ? item.w : item.h;
433
- }
434
- // Justify: compute starting offset and extra spacing
435
- let mainOffset = 0;
436
- let extraGap = 0;
437
- switch (justify) {
438
- case 'start':
439
- break;
440
- case 'center':
441
- mainOffset = Math.max(0, (mainSize - totalMain) / 2);
442
- break;
443
- case 'end':
444
- mainOffset = Math.max(0, mainSize - totalMain);
445
- break;
446
- case 'space-between':
447
- if (items.length > 1) {
448
- extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
449
- }
450
- break;
451
- case 'space-around':
452
- if (items.length > 0) {
453
- const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
454
- const segment = totalSpace / items.length;
455
- mainOffset = segment / 2;
456
- extraGap = segment - gap;
457
- }
458
- break;
459
- }
460
- // Position each item
461
- let pos = mainOffset;
462
- for (const item of items) {
463
- const mainDim = isRow ? item.w : item.h;
464
- const crossDim = isRow ? item.h : item.w;
465
- // Cross-axis alignment (alignSelf overrides align)
466
- const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
467
- ? item.child._flexConfig.alignSelf
468
- : align;
469
- let crossPos = crossOffset;
470
- switch (effectiveAlign) {
471
- case 'start':
472
- break;
473
- case 'center':
474
- crossPos += (crossSize - crossDim) / 2;
475
- break;
476
- case 'end':
477
- crossPos += crossSize - crossDim;
478
- break;
479
- case 'stretch':
480
- setChildCrossSize(item.child, isRow, crossSize);
481
- break;
482
- }
483
- // Compensate for local bounds offset (e.g. centered anchors)
484
- if (isRow) {
485
- item.child.x = pos - item.ox;
486
- item.child.y = crossPos - item.oy;
487
- }
488
- else {
489
- item.child.x = crossPos - item.ox;
490
- item.child.y = pos - item.oy;
491
- }
492
- pos += mainDim + gap + extraGap;
493
- }
494
- }
495
- // ─── FlexContainer ───────────────────────────────────────
496
- /**
497
- * Lightweight flexbox-like layout container for PixiJS.
498
- *
499
- * Supports row/column direction, justify/align, gap, padding, wrapping,
500
- * and flex-grow distribution. Zero external dependencies.
501
- *
502
- * @example
503
- * ```ts
504
- * const toolbar = new FlexContainer({
505
- * direction: 'row',
506
- * justifyContent: 'space-between',
507
- * alignItems: 'center',
508
- * gap: 16,
509
- * padding: 12,
510
- * });
511
- *
512
- * toolbar.addFlexChild(button1);
513
- * toolbar.addFlexChild(button2);
514
- * toolbar.resize(800, 60);
515
- * ```
516
- */
517
- class FlexContainer extends pixi_js.Container {
518
- __uiComponent = true;
519
- _config;
520
- _padding;
521
- _maxWidth;
522
- _maxHeight;
523
- /** @internal */ _explicitWidth;
524
- /** @internal */ _explicitHeight;
525
- /** @internal */ _computedWidth = 0;
526
- /** @internal */ _computedHeight = 0;
527
- /** @internal */ _availableWidth = 0;
528
- /** @internal */ _availableHeight = 0;
529
- /** @internal */ _rawWidth;
530
- /** @internal */ _rawHeight;
531
- _layoutChildren = [];
532
- _layoutDirty = true;
533
- _layoutSuspended = false;
534
- constructor(config = {}) {
535
- super();
536
- this._config = {
537
- direction: config.direction ?? 'row',
538
- justifyContent: config.justifyContent ?? 'start',
539
- alignItems: config.alignItems ?? 'start',
540
- gap: config.gap ?? 0,
541
- flexWrap: config.flexWrap ?? false,
542
- alignContent: config.alignContent ?? 'start',
543
- };
544
- this._padding = resolvePadding(config);
545
- this._maxWidth = config.maxWidth ?? Infinity;
546
- this._maxHeight = config.maxHeight ?? Infinity;
547
- this._rawWidth = config.width ?? 0;
548
- this._rawHeight = config.height ?? 0;
549
- this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
550
- this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
551
- }
552
- // ─── Public API ──────────────────────────────────────
553
- /** Add a child with optional flex config. Also registers in flex layout. */
554
- addFlexChild(child, flexConfig) {
555
- if (flexConfig)
556
- child._flexConfig = flexConfig;
557
- if (!this._layoutChildren.includes(child)) {
558
- this._layoutChildren.push(child);
559
- this._layoutDirty = true;
560
- }
561
- super.addChild(child);
562
- return this;
563
- }
564
- /** Remove a child from flex layout and display list */
565
- removeFlexChild(child) {
566
- const idx = this._layoutChildren.indexOf(child);
567
- if (idx !== -1) {
568
- this._layoutChildren.splice(idx, 1);
569
- this._layoutDirty = true;
570
- }
571
- super.removeChild(child);
572
- return this;
573
- }
574
- /** Remove all flex children */
575
- clearFlexChildren() {
576
- for (const child of this._layoutChildren) {
577
- super.removeChild(child);
578
- }
579
- this._layoutChildren.length = 0;
580
- this._layoutDirty = true;
581
- return this;
582
- }
583
- /**
584
- * Override addChild so children automatically participate in flex layout.
585
- * This enables declarative usage from React JSX.
586
- */
587
- addChild(...children) {
588
- for (const child of children) {
589
- if (!this._layoutChildren.includes(child)) {
590
- this._layoutChildren.push(child);
591
- this._layoutDirty = true;
592
- }
593
- }
594
- const result = super.addChild(...children);
595
- if (this._layoutDirty && !this._layoutSuspended)
596
- this.updateLayout();
597
- return result;
598
- }
599
- addChildAt(child, index) {
600
- if (!this._layoutChildren.includes(child)) {
601
- // Insert into layout children at matching position
602
- const layoutIndex = Math.min(index, this._layoutChildren.length);
603
- this._layoutChildren.splice(layoutIndex, 0, child);
604
- this._layoutDirty = true;
605
- }
606
- const result = super.addChildAt(child, index);
607
- if (this._layoutDirty && !this._layoutSuspended)
608
- this.updateLayout();
609
- return result;
610
- }
611
- removeChild(...children) {
612
- for (const child of children) {
613
- const idx = this._layoutChildren.indexOf(child);
614
- if (idx !== -1) {
615
- this._layoutChildren.splice(idx, 1);
616
- this._layoutDirty = true;
617
- }
618
- }
619
- return super.removeChild(...children);
620
- }
621
- /** Get all flex layout children (read-only) */
622
- get flexChildren() {
623
- return this._layoutChildren;
624
- }
625
- /** Suspend automatic layout recalculation. Call resumeLayout() to flush. */
626
- suspendLayout() {
627
- this._layoutSuspended = true;
628
- }
629
- /** Resume automatic layout and flush if dirty. */
630
- resumeLayout() {
631
- this._layoutSuspended = false;
632
- if (this._layoutDirty)
633
- this.updateLayout();
634
- }
635
- /** Update the container size and recalculate layout */
636
- resize(width, height) {
637
- this._explicitWidth = width;
638
- this._explicitHeight = height;
639
- this._layoutDirty = true;
640
- if (!this._layoutSuspended)
641
- this.updateLayout();
642
- }
643
- /** Update layout direction */
644
- setDirection(direction) {
645
- this._config.direction = direction;
646
- this._layoutDirty = true;
647
- }
648
- /** Update justifyContent */
649
- setJustifyContent(justify) {
650
- this._config.justifyContent = justify;
651
- this._layoutDirty = true;
652
- }
653
- /** Update alignItems */
654
- setAlignItems(align) {
655
- this._config.alignItems = align;
656
- this._layoutDirty = true;
657
- }
658
- /** Update gap */
659
- setGap(gap) {
660
- this._config.gap = gap;
661
- this._layoutDirty = true;
662
- }
663
- /** Update padding */
664
- setPadding(padding) {
665
- this._padding = normalizePadding(padding);
666
- this._layoutDirty = true;
667
- }
668
- /**
669
- * Recalculate and apply layout positions for all children.
670
- * Called automatically by `resize()`. Call manually after
671
- * adding/removing children without resize.
672
- */
673
- updateLayout() {
674
- if (this._layoutSuspended) {
675
- this._layoutDirty = true;
676
- return;
677
- }
678
- this._layoutDirty = false;
679
- const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
680
- const [pt, pr, pb, pl] = this._padding;
681
- const isRow = direction === 'row';
682
- // Resolve percentage width/height against parent's available space
683
- if (typeof this._rawWidth === 'string') {
684
- this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
685
- }
686
- if (typeof this._rawHeight === 'string') {
687
- this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
688
- }
689
- const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
690
- const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
691
- const mainLimit = isRow ? contentW : contentH;
692
- const crossLimit = isRow ? contentH : contentW;
693
- // Pass content area to measureChild for percentage resolution
694
- const pctRefW = contentW < Infinity ? contentW : 0;
695
- const pctRefH = contentH < Infinity ? contentH : 0;
696
- // Propagate available size to child FlexContainers and resolve their percentages
697
- for (const child of this._layoutChildren) {
698
- if (child instanceof FlexContainer) {
699
- const fc = child;
700
- fc._availableWidth = pctRefW;
701
- fc._availableHeight = pctRefH;
702
- // If child has percentage dimensions, trigger its layout to resolve them
703
- if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
704
- fc.updateLayout();
705
- }
706
- }
707
- }
708
- // Measure children (skip flexExclude — they position themselves)
709
- const measured = [];
710
- for (const child of this._layoutChildren) {
711
- if (child._flexConfig?.flexExclude)
712
- continue;
713
- const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
714
- measured.push({ child, w, h, ox, oy });
715
- }
716
- // Split into lines (if wrapping)
717
- const lines = [];
718
- if (flexWrap && mainLimit < Infinity) {
719
- let currentLine = [];
720
- let lineMain = 0;
721
- for (const item of measured) {
722
- const itemMain = isRow ? item.w : item.h;
723
- const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
724
- if (currentLine.length > 0 && wouldBe > mainLimit) {
725
- lines.push(currentLine);
726
- currentLine = [item];
727
- lineMain = itemMain;
728
- }
729
- else {
730
- currentLine.push(item);
731
- lineMain = wouldBe;
732
- }
733
- }
734
- if (currentLine.length > 0)
735
- lines.push(currentLine);
736
- }
737
- else {
738
- lines.push(measured);
739
- }
740
- // Compute cross size per line
741
- const lineCrossSizes = lines.map((line) => {
742
- let maxCross = 0;
743
- for (const item of line) {
744
- const cross = isRow ? item.h : item.w;
745
- if (cross > maxCross)
746
- maxCross = cross;
747
- }
748
- return maxCross;
749
- });
750
- // Compute natural main size (for auto-sizing when no explicit size given)
751
- let naturalMainSize = 0;
752
- if (mainLimit === Infinity) {
753
- for (const line of lines) {
754
- let lineMain = 0;
755
- for (const item of line) {
756
- lineMain += isRow ? item.w : item.h;
757
- }
758
- lineMain += gap * Math.max(0, line.length - 1);
759
- naturalMainSize = Math.max(naturalMainSize, lineMain);
760
- }
761
- }
762
- // Effective main size: explicit if set, otherwise natural content size
763
- const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
764
- // Compute alignContent offsets for multi-line layouts
765
- const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
766
- let acOffset = 0;
767
- let acExtraGap = 0;
768
- if (lines.length > 1 && crossLimit < Infinity) {
769
- const freeSpace = Math.max(0, crossLimit - totalLinesCross);
770
- switch (alignContent) {
771
- case 'center':
772
- acOffset = freeSpace / 2;
773
- break;
774
- case 'end':
775
- acOffset = freeSpace;
776
- break;
777
- case 'space-between':
778
- if (lines.length > 1) {
779
- acExtraGap = freeSpace / (lines.length - 1);
780
- }
781
- break;
782
- case 'stretch':
783
- if (lines.length > 0) {
784
- const extra = freeSpace / lines.length;
785
- for (let i = 0; i < lineCrossSizes.length; i++) {
786
- lineCrossSizes[i] += extra;
787
- }
788
- }
789
- break;
790
- // 'start' — no adjustment
791
- }
792
- }
793
- // Layout each line
794
- let crossOffset = (isRow ? pt : pl) + acOffset;
795
- for (let i = 0; i < lines.length; i++) {
796
- const line = lines[i];
797
- const lineCross = lineCrossSizes[i];
798
- const mainStart = isRow ? pl : pt;
799
- // Offset items by padding
800
- const tempItems = line.map((item) => ({ ...item }));
801
- // Cross size for alignment: use container cross size for single-line, line cross for multi-line
802
- const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
803
- layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
804
- // Apply main-axis padding offset
805
- for (const item of tempItems) {
806
- const origChild = line.find((l) => l.child === item.child);
807
- origChild.child.x = item.child.x + (isRow ? mainStart : 0);
808
- origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
809
- }
810
- crossOffset += lineCross + gap + acExtraGap;
811
- }
812
- // Compute and store actual dimensions for measureChild() and getContentSize()
813
- let totalCrossNatural = 0;
814
- for (let i = 0; i < lineCrossSizes.length; i++) {
815
- totalCrossNatural += lineCrossSizes[i];
816
- if (i < lineCrossSizes.length - 1)
817
- totalCrossNatural += gap;
818
- }
819
- if (isRow) {
820
- this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
821
- this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
822
- }
823
- else {
824
- this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
825
- this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
826
- }
827
- // Position flexExclude children (absolute positioning).
828
- // All position props describe the visual (bounds) rectangle. We derive the
829
- // visual rectangle directly from `getLocalBounds()` rather than the
830
- // layout-config `w`/`h` returned by measureChild — those may differ from
831
- // actual bounds when the user pins `layoutWidth`/`layoutHeight` to a value
832
- // that doesn't match the child's real visual extent (e.g. a Button whose
833
- // text overflows its configured `width`). Mixing layout size with real
834
- // bounds-origin would shift the visual center off the requested point.
835
- // `centerX/centerY` take precedence over `left/right` / `top/bottom` on each axis.
836
- for (const child of this._layoutChildren) {
837
- if (!child._flexConfig?.flexExclude)
838
- continue;
839
- const cfg = child._flexConfig;
840
- const bounds = child.getLocalBounds();
841
- const cw = this._computedWidth;
842
- const ch = this._computedHeight;
843
- const centerX = resolveDimension(cfg.centerX, pctRefW);
844
- if (centerX !== undefined)
845
- child.x = centerX - bounds.x - bounds.width / 2;
846
- else if (cfg.left !== undefined)
847
- child.x = cfg.left - bounds.x;
848
- else if (cfg.right !== undefined)
849
- child.x = cw - cfg.right - bounds.x - bounds.width;
850
- const centerY = resolveDimension(cfg.centerY, pctRefH);
851
- if (centerY !== undefined)
852
- child.y = centerY - bounds.y - bounds.height / 2;
853
- else if (cfg.top !== undefined)
854
- child.y = cfg.top - bounds.y;
855
- else if (cfg.bottom !== undefined)
856
- child.y = ch - cfg.bottom - bounds.y - bounds.height;
857
- }
858
- }
859
- /** Computed content size (after layout) */
860
- getContentSize() {
861
- if (this._layoutDirty)
862
- this.updateLayout();
863
- return { width: this._computedWidth, height: this._computedHeight };
864
- }
865
- /** React reconciler update hook — applies changed config props */
866
- updateConfig(changed) {
867
- if ('direction' in changed)
868
- this.setDirection(changed.direction);
869
- if ('justifyContent' in changed)
870
- this.setJustifyContent(changed.justifyContent);
871
- if ('alignItems' in changed)
872
- this.setAlignItems(changed.alignItems);
873
- if ('gap' in changed)
874
- this.setGap(changed.gap);
875
- if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
876
- this._padding = resolvePadding(changed);
877
- this._layoutDirty = true;
878
- }
879
- if ('flexWrap' in changed) {
880
- this._config.flexWrap = changed.flexWrap;
881
- this._layoutDirty = true;
882
- }
883
- if ('alignContent' in changed) {
884
- this._config.alignContent = changed.alignContent;
885
- this._layoutDirty = true;
886
- }
887
- if ('width' in changed || 'height' in changed) {
888
- const w = changed.width ?? this._rawWidth;
889
- const h = changed.height ?? this._rawHeight;
890
- this._rawWidth = w;
891
- this._rawHeight = h;
892
- if (typeof w === 'number' && typeof h === 'number') {
893
- this.resize(w, h);
894
- }
895
- else {
896
- // Percentage — will resolve in updateLayout
897
- this._explicitWidth = typeof w === 'number' ? w : 0;
898
- this._explicitHeight = typeof h === 'number' ? h : 0;
899
- this._layoutDirty = true;
900
- if (!this._layoutSuspended)
901
- this.updateLayout();
902
- }
903
- return;
904
- }
905
- if (this._layoutDirty && !this._layoutSuspended)
906
- this.updateLayout();
907
- }
908
- destroy(options) {
909
- this._layoutChildren.length = 0;
910
- super.destroy(options);
911
- }
912
- }
913
-
914
- /** Flex item prop names that should be forwarded to _flexConfig on the child */
915
- const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude', 'top', 'right', 'bottom', 'left', 'centerX', 'centerY'];
916
- function isSuspendable(obj) {
917
- return typeof obj?.suspendLayout === 'function' && typeof obj?.resumeLayout === 'function';
918
- }
919
- /** Layout containers that need flush after commit phase */
920
- const pendingLayoutFlush = new Set();
921
- /** Suspend a layout container and all its suspendable ancestors, add them to pending flush */
922
- function suspendWithAncestors(instance) {
923
- let current = instance;
924
- while (current) {
925
- if (isSuspendable(current)) {
926
- current.suspendLayout();
927
- pendingLayoutFlush.add(current);
928
- }
929
- current = current.parent;
930
- }
931
- }
932
- /** Extract FlexItemConfig from props if any flex item props are present */
933
- function extractFlexItemConfig(props) {
934
- let config;
935
- for (const key of FLEX_ITEM_PROPS) {
936
- if (key in props) {
937
- if (!config)
938
- config = {};
939
- config[key] = props[key];
940
- }
941
- }
942
- return config;
943
- }
944
- /** Apply flex item config to a child being added to a FlexContainer */
945
- function addChildToFlex(parent, child) {
946
- const flexConfig = child._flexConfig;
947
- if (flexConfig && Object.keys(flexConfig).length > 0) {
948
- parent.addFlexChild(child, flexConfig);
949
- }
950
- else {
951
- parent.addChild(child);
952
- }
953
- }
954
- function toPascalCase(str) {
955
- return str.charAt(0).toUpperCase() + str.slice(1);
956
- }
957
- const hostConfig = {
958
- isPrimaryRenderer: false,
959
- supportsMutation: true,
960
- supportsPersistence: false,
961
- supportsHydration: false,
962
- createInstance(type, props) {
963
- const name = toPascalCase(type);
964
- const Ctor = catalogue[name];
965
- if (!Ctor) {
966
- throw new Error(`[PixiReconciler] Unknown element "<${type}>". ` +
967
- `Call extend({ ${name} }) before rendering.`);
968
- }
969
- let instance;
970
- if (typeof Ctor.prototype.updateConfig === 'function') {
971
- // Config-based UI component: pass props as constructor config
972
- const config = extractConfig(props);
973
- instance = new Ctor(config);
974
- applyContainerProps(instance, props);
975
- applyEventProps(instance, props);
976
- }
977
- else {
978
- // Standard PixiJS element
979
- instance = new Ctor();
980
- applyProps(instance, props);
981
- }
982
- if (hasEventProps(props) && instance.eventMode === 'auto') {
983
- instance.eventMode = 'static';
984
- }
985
- // Store flex item config for when this child is added to a FlexContainer parent
986
- const flexItemConfig = extractFlexItemConfig(props);
987
- if (flexItemConfig) {
988
- instance._flexConfig = { ...instance._flexConfig, ...flexItemConfig };
989
- }
990
- // Auto-set layoutWidth/layoutHeight from explicit width/height props for UI components.
991
- // This ensures parent FlexContainers measure children correctly without relying on
992
- // getLocalBounds() which may return wrong values before the first render pass
993
- // (e.g. Text with unloaded fonts, Graphics with pending geometry).
994
- if (typeof Ctor.prototype.updateConfig === 'function' && !(instance instanceof FlexContainer)) {
995
- const w = typeof props.width === 'number' ? props.width : undefined;
996
- const h = typeof props.height === 'number' ? props.height : undefined;
997
- if (w !== undefined || h !== undefined) {
998
- if (!instance._flexConfig)
999
- instance._flexConfig = {};
1000
- if (w !== undefined && instance._flexConfig.layoutWidth === undefined) {
1001
- instance._flexConfig.layoutWidth = w;
1002
- }
1003
- if (h !== undefined && instance._flexConfig.layoutHeight === undefined) {
1004
- instance._flexConfig.layoutHeight = h;
1005
- }
1006
- }
1007
- }
1008
- return instance;
1009
- },
1010
- createTextInstance() {
1011
- throw new Error('[PixiReconciler] Text strings are not supported. Use a <text> element.');
1012
- },
1013
- appendInitialChild(parent, child) {
1014
- if (child instanceof pixi_js.Container) {
1015
- if (isSuspendable(parent))
1016
- parent.suspendLayout();
1017
- if (parent instanceof FlexContainer) {
1018
- addChildToFlex(parent, child);
1019
- }
1020
- else {
1021
- parent.addChild(child);
1022
- }
1023
- }
1024
- },
1025
- appendChild(parent, child) {
1026
- if (child instanceof pixi_js.Container) {
1027
- if (isSuspendable(parent)) {
1028
- parent.suspendLayout();
1029
- pendingLayoutFlush.add(parent);
1030
- }
1031
- if (parent instanceof FlexContainer) {
1032
- addChildToFlex(parent, child);
1033
- }
1034
- else {
1035
- parent.addChild(child);
1036
- }
1037
- }
1038
- },
1039
- appendChildToContainer(container, child) {
1040
- if (child instanceof pixi_js.Container)
1041
- container.addChild(child);
1042
- },
1043
- removeChild(parent, child) {
1044
- if (child instanceof pixi_js.Container) {
1045
- if (isSuspendable(parent)) {
1046
- parent.suspendLayout();
1047
- pendingLayoutFlush.add(parent);
1048
- }
1049
- parent.removeChild(child);
1050
- child.destroy({ children: true });
1051
- }
1052
- },
1053
- removeChildFromContainer(container, child) {
1054
- if (child instanceof pixi_js.Container) {
1055
- container.removeChild(child);
1056
- child.destroy({ children: true });
1057
- }
1058
- },
1059
- insertBefore(parent, child, beforeChild) {
1060
- if (child instanceof pixi_js.Container && beforeChild instanceof pixi_js.Container) {
1061
- if (child.parent)
1062
- child.parent.removeChild(child);
1063
- if (isSuspendable(parent)) {
1064
- parent.suspendLayout();
1065
- pendingLayoutFlush.add(parent);
1066
- }
1067
- const index = parent.getChildIndex(beforeChild);
1068
- parent.addChildAt(child, index);
1069
- }
1070
- },
1071
- insertInContainerBefore(container, child, beforeChild) {
1072
- if (child instanceof pixi_js.Container && beforeChild instanceof pixi_js.Container) {
1073
- if (child.parent)
1074
- child.parent.removeChild(child);
1075
- const index = container.getChildIndex(beforeChild);
1076
- container.addChildAt(child, index);
1077
- }
1078
- },
1079
- commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
1080
- // Suspend this instance and ancestors before applying changes —
1081
- // prevents intermediate relayouts with partially-updated tree
1082
- const dimensionsChanged = newProps.width !== oldProps.width || newProps.height !== oldProps.height;
1083
- if (isSuspendable(instance) || dimensionsChanged) {
1084
- suspendWithAncestors(instance);
1085
- }
1086
- if (typeof instance.updateConfig === 'function') {
1087
- const changed = diffConfig(newProps, oldProps);
1088
- if (Object.keys(changed).length > 0) {
1089
- instance.updateConfig(changed);
1090
- }
1091
- applyContainerProps(instance, newProps, oldProps);
1092
- applyEventProps(instance, newProps, oldProps);
1093
- }
1094
- else {
1095
- applyProps(instance, newProps, oldProps);
1096
- }
1097
- // Update flex item config if parent is FlexContainer
1098
- const newFlexConfig = extractFlexItemConfig(newProps);
1099
- const oldFlexConfig = extractFlexItemConfig(oldProps);
1100
- if (newFlexConfig || oldFlexConfig) {
1101
- instance._flexConfig = { ...instance._flexConfig, ...newFlexConfig };
1102
- }
1103
- // Keep auto layoutWidth/layoutHeight in sync with width/height for UI components
1104
- if (typeof instance.updateConfig === 'function' && !(instance instanceof FlexContainer)) {
1105
- if (instance._flexConfig) {
1106
- if (typeof newProps.width === 'number' && instance._flexConfig.layoutWidth !== undefined) {
1107
- instance._flexConfig.layoutWidth = newProps.width;
1108
- }
1109
- if (typeof newProps.height === 'number' && instance._flexConfig.layoutHeight !== undefined) {
1110
- instance._flexConfig.layoutHeight = newProps.height;
1111
- }
1112
- }
1113
- }
1114
- // Mark ancestors dirty if flex config or dimensions changed
1115
- if (newFlexConfig || oldFlexConfig || dimensionsChanged) {
1116
- suspendWithAncestors(instance);
1117
- }
1118
- if (hasEventProps(newProps) && instance.eventMode === 'auto') {
1119
- instance.eventMode = 'static';
1120
- }
1121
- },
1122
- finalizeInitialChildren(instance) {
1123
- // Resume layout after all initial children have been appended
1124
- if (isSuspendable(instance)) {
1125
- instance.resumeLayout();
1126
- }
1127
- return false;
1128
- },
1129
- prepareUpdate() {
1130
- return true;
1131
- },
1132
- shouldSetTextContent() {
1133
- return false;
1134
- },
1135
- getRootHostContext() {
1136
- return null;
1137
- },
1138
- getChildHostContext(parentHostContext) {
1139
- return parentHostContext;
1140
- },
1141
- getPublicInstance(instance) {
1142
- return instance;
1143
- },
1144
- prepareForCommit() {
1145
- return null;
1146
- },
1147
- resetAfterCommit() {
1148
- if (pendingLayoutFlush.size === 0)
1149
- return;
1150
- // Sort by depth (deepest first) so children compute sizes before parents lay out
1151
- const sorted = [...pendingLayoutFlush].sort((a, b) => {
1152
- let da = 0;
1153
- let n = a;
1154
- while (n.parent) {
1155
- da++;
1156
- n = n.parent;
1157
- }
1158
- let db = 0;
1159
- n = b;
1160
- while (n.parent) {
1161
- db++;
1162
- n = n.parent;
1163
- }
1164
- return db - da; // deepest first
1165
- });
1166
- for (const fc of sorted) {
1167
- fc.resumeLayout();
1168
- }
1169
- pendingLayoutFlush.clear();
1170
- },
1171
- preparePortalMount() { },
1172
- scheduleTimeout: setTimeout,
1173
- cancelTimeout: clearTimeout,
1174
- noTimeout: -1,
1175
- getCurrentEventPriority() {
1176
- return constants.DefaultEventPriority;
1177
- },
1178
- hideInstance(instance) {
1179
- instance.visible = false;
1180
- },
1181
- unhideInstance(instance) {
1182
- instance.visible = true;
1183
- },
1184
- hideTextInstance() { },
1185
- unhideTextInstance() { },
1186
- clearContainer() { },
1187
- detachDeletedInstance() { },
1188
- prepareScopeUpdate() { },
1189
- getInstanceFromNode() { return null; },
1190
- getInstanceFromScope() { return null; },
1191
- beforeActiveInstanceBlur() { },
1192
- afterActiveInstanceBlur() { },
1193
- };
1194
- const reconciler = Reconciler(hostConfig);
1195
-
1196
- function createPixiRoot(container) {
1197
- const fiberRoot = reconciler.createContainer(container, // containerInfo
1198
- constants.ConcurrentRoot, // tag
1199
- null, // hydrationCallbacks
1200
- false, // isStrictMode
1201
- null, // concurrentUpdatesByDefaultOverride
1202
- '', // identifierPrefix
1203
- (err) => console.error('[PixiRoot]', err), null);
1204
- return {
1205
- render(element) {
1206
- reconciler.updateContainer(element, fiberRoot, null, () => { });
1207
- },
1208
- unmount() {
1209
- reconciler.updateContainer(null, fiberRoot, null, () => { });
1210
- },
1211
- };
1212
- }
1213
-
1214
- /**
1215
- * Resolve a ViewInput to a Container instance.
1216
- *
1217
- * @example
1218
- * ```ts
1219
- * resolveView('btn-idle') // → Sprite.from('btn-idle')
1220
- * resolveView(someTexture) // → new Sprite(someTexture)
1221
- * resolveView(myCustomContainer) // → myCustomContainer (as-is)
1222
- * resolveView(undefined) // → null
1223
- * ```
1224
- */
1225
- function resolveView(input) {
1226
- if (input == null)
1227
- return null;
1228
- if (typeof input === 'string')
1229
- return pixi_js.Sprite.from(input);
1230
- if (input instanceof pixi_js.Texture)
1231
- return new pixi_js.Sprite(input);
1232
- return input;
1233
- }
1234
-
1235
- /**
1236
- * Collection of easing functions for use with Tween and Timeline.
1237
- *
1238
- * All functions take a progress value t (0..1) and return the eased value.
1239
- */
1240
- const Easing = {
1241
- easeOutQuad: (t) => t * (2 - t),
1242
- easeInCubic: (t) => t * t * t,
1243
- easeOutCubic: (t) => --t * t * t + 1,
1244
- easeOutBack: (t) => {
1245
- const c1 = 1.70158;
1246
- const c3 = c1 + 1;
1247
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1248
- }};
1249
-
1250
- /**
1251
- * Lightweight tween system integrated with PixiJS Ticker.
1252
- * Zero external dependencies — no GSAP required.
1253
- *
1254
- * All tweens return a Promise that resolves on completion.
1255
- *
1256
- * @example
1257
- * ```ts
1258
- * // Fade in a sprite
1259
- * await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
1260
- *
1261
- * // Move and wait
1262
- * await Tween.to(sprite, { x: 500 }, 300);
1263
- *
1264
- * // From a starting value
1265
- * await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
1266
- * ```
1267
- */
1268
- class Tween {
1269
- static _tweens = [];
1270
- static _tickerAdded = false;
1271
- /**
1272
- * Animate properties from current values to target values.
1273
- *
1274
- * @param target - Object to animate (Sprite, Container, etc.)
1275
- * @param props - Target property values
1276
- * @param duration - Duration in milliseconds
1277
- * @param easing - Easing function (default: easeOutQuad)
1278
- * @param onUpdate - Progress callback (0..1)
1279
- */
1280
- static to(target, props, duration, easing, onUpdate) {
1281
- // A destroyed (Pixi) target has null transform fields — skip rather than throw. This guards
1282
- // animations whose target is torn down mid-flight (e.g. a reel grid rebuilt during a spin).
1283
- if (target == null || target.destroyed)
1284
- return Promise.resolve();
1285
- return new Promise((resolve) => {
1286
- // Capture starting values
1287
- const from = {};
1288
- for (const key of Object.keys(props)) {
1289
- from[key] = Tween.getProperty(target, key);
1290
- }
1291
- const tween = {
1292
- target,
1293
- from,
1294
- to: { ...props },
1295
- duration: Math.max(1, duration),
1296
- easing: easing ?? Easing.easeOutQuad,
1297
- elapsed: 0,
1298
- delay: 0,
1299
- resolve,
1300
- onUpdate,
1301
- };
1302
- Tween._tweens.push(tween);
1303
- Tween.ensureTicker();
1304
- });
1305
- }
1306
- /**
1307
- * Animate properties from given values to current values.
1308
- */
1309
- static from(target, props, duration, easing, onUpdate) {
1310
- if (target == null || target.destroyed)
1311
- return Promise.resolve();
1312
- // Capture current values as "to"
1313
- const to = {};
1314
- for (const key of Object.keys(props)) {
1315
- to[key] = Tween.getProperty(target, key);
1316
- Tween.setProperty(target, key, props[key]);
1317
- }
1318
- return Tween.to(target, to, duration, easing, onUpdate);
1319
- }
1320
- /**
1321
- * Animate from one set of values to another.
1322
- */
1323
- static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
1324
- if (target == null || target.destroyed)
1325
- return Promise.resolve();
1326
- // Set starting values
1327
- for (const key of Object.keys(fromProps)) {
1328
- Tween.setProperty(target, key, fromProps[key]);
1329
- }
1330
- return Tween.to(target, toProps, duration, easing, onUpdate);
1331
- }
1332
- /**
1333
- * Wait for a given duration (useful in timelines).
1334
- * Uses PixiJS Ticker for consistent timing with other tweens.
1335
- */
1336
- static delay(ms) {
1337
- return new Promise((resolve) => {
1338
- let elapsed = 0;
1339
- const onTick = (ticker) => {
1340
- elapsed += ticker.deltaMS;
1341
- if (elapsed >= ms) {
1342
- pixi_js.Ticker.shared.remove(onTick);
1343
- resolve();
1344
- }
1345
- };
1346
- pixi_js.Ticker.shared.add(onTick);
1347
- });
1348
- }
1349
- /**
1350
- * Kill all tweens on a target.
1351
- */
1352
- static killTweensOf(target) {
1353
- Tween._tweens = Tween._tweens.filter((tw) => {
1354
- if (tw.target === target) {
1355
- tw.resolve();
1356
- return false;
1357
- }
1358
- return true;
1359
- });
1360
- }
1361
- /**
1362
- * Kill all active tweens.
1363
- */
1364
- static killAll() {
1365
- for (const tw of Tween._tweens) {
1366
- tw.resolve();
1367
- }
1368
- Tween._tweens.length = 0;
1369
- }
1370
- /** Number of active tweens */
1371
- static get activeTweens() {
1372
- return Tween._tweens.length;
1373
- }
1374
- /**
1375
- * Reset the tween system — kill all tweens and remove the ticker.
1376
- * Useful for cleanup between game instances, tests, or hot-reload.
1377
- */
1378
- static reset() {
1379
- for (const tw of Tween._tweens) {
1380
- tw.resolve();
1381
- }
1382
- Tween._tweens.length = 0;
1383
- if (Tween._tickerAdded) {
1384
- pixi_js.Ticker.shared.remove(Tween.tick);
1385
- Tween._tickerAdded = false;
1386
- }
1387
- }
1388
- // ─── Internal ──────────────────────────────────────────
1389
- static ensureTicker() {
1390
- if (Tween._tickerAdded)
1391
- return;
1392
- Tween._tickerAdded = true;
1393
- pixi_js.Ticker.shared.add(Tween.tick);
1394
- }
1395
- static tick = (ticker) => {
1396
- const dt = ticker.deltaMS;
1397
- const completed = [];
1398
- for (const tw of Tween._tweens) {
1399
- // target torn down mid-tween → finish it quietly
1400
- if (tw.target?.destroyed) {
1401
- completed.push(tw);
1402
- continue;
1403
- }
1404
- tw.elapsed += dt;
1405
- if (tw.elapsed < tw.delay)
1406
- continue;
1407
- const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
1408
- const t = tw.easing(raw);
1409
- // Interpolate each property
1410
- for (const key of Object.keys(tw.to)) {
1411
- const start = tw.from[key];
1412
- const end = tw.to[key];
1413
- const value = start + (end - start) * t;
1414
- Tween.setProperty(tw.target, key, value);
1415
- }
1416
- tw.onUpdate?.(raw);
1417
- if (raw >= 1) {
1418
- completed.push(tw);
1419
- }
1420
- }
1421
- // Remove completed tweens
1422
- for (const tw of completed) {
1423
- const idx = Tween._tweens.indexOf(tw);
1424
- if (idx !== -1)
1425
- Tween._tweens.splice(idx, 1);
1426
- tw.resolve();
1427
- }
1428
- // Remove ticker when no active tweens
1429
- if (Tween._tweens.length === 0 && Tween._tickerAdded) {
1430
- pixi_js.Ticker.shared.remove(Tween.tick);
1431
- Tween._tickerAdded = false;
1432
- }
1433
- };
1434
- /**
1435
- * Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
1436
- */
1437
- static getProperty(target, key) {
1438
- const parts = key.split('.');
1439
- let obj = target;
1440
- for (let i = 0; i < parts.length - 1; i++) {
1441
- obj = obj?.[parts[i]];
1442
- }
1443
- return obj?.[parts[parts.length - 1]] ?? 0;
1444
- }
1445
- /**
1446
- * Set a potentially nested property.
1447
- */
1448
- static setProperty(target, key, value) {
1449
- const parts = key.split('.');
1450
- let obj = target;
1451
- for (let i = 0; i < parts.length - 1; i++) {
1452
- obj = obj?.[parts[i]];
1453
- }
1454
- if (obj == null)
1455
- return;
1456
- obj[parts[parts.length - 1]] = value;
1457
- }
1458
- }
1459
-
1460
- const DEFAULT_COLORS = {
1461
- default: 0xffd700,
1462
- hover: 0xffe44d,
1463
- pressed: 0xccac00,
1464
- disabled: 0x666666,
1465
- };
1466
- function makeGraphicsView(w, h, radius, color) {
1467
- const g = new pixi_js.Graphics();
1468
- g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
1469
- return g;
1470
- }
1471
- /**
1472
- * Interactive button with per-state custom views and animations.
1473
- *
1474
- * Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
1475
- * (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
1476
- * Falls back to colored Graphics when no custom view is provided.
1477
- *
1478
- * @example
1479
- * ```ts
1480
- * // Graphics-based (quick prototyping)
1481
- * const btn = new Button({
1482
- * width: 200, height: 60, borderRadius: 12,
1483
- * colors: { default: 0x22aa22, hover: 0x33cc33 },
1484
- * text: 'SPIN',
1485
- * onPress: () => spin(),
1486
- * });
1487
- *
1488
- * // Asset-based (production art)
1489
- * const btn = new Button({
1490
- * defaultView: 'btn-idle',
1491
- * hoverView: 'btn-hover',
1492
- * pressedView: 'btn-pressed',
1493
- * disabledView: 'btn-disabled',
1494
- * text: 'SPIN',
1495
- * onPress: () => spin(),
1496
- * });
1497
- *
1498
- * // Custom Container view
1499
- * const btn = new Button({
1500
- * defaultView: myAnimatedSprite,
1501
- * text: 'SPIN',
1502
- * });
1503
- * ```
1504
- */
1505
- class Button extends pixi_js.Container {
1506
- __uiComponent = true;
1507
- _views = new Map();
1508
- _state = 'default';
1509
- _enabled = true;
1510
- _config;
1511
- _textObj = null;
1512
- /** Press callback */
1513
- onPress;
1514
- constructor(config = {}) {
1515
- super();
1516
- this._config = {
1517
- width: config.width ?? 200,
1518
- height: config.height ?? 60,
1519
- borderRadius: config.borderRadius ?? 8,
1520
- pressScale: config.pressScale ?? 0.95,
1521
- animationDuration: config.animationDuration ?? 100,
1522
- ...config,
1523
- };
1524
- this.onPress = config.onPress;
1525
- this._buildViews(config);
1526
- // Text
1527
- if (config.text) {
1528
- this._textObj = new pixi_js.Text({
1529
- text: config.text,
1530
- style: {
1531
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1532
- fontSize: 20,
1533
- fill: 0xffffff,
1534
- fontWeight: 'bold',
1535
- ...config.textStyle,
1536
- },
1537
- });
1538
- this._textObj.anchor.set(0.5);
1539
- this.addChild(this._textObj);
1540
- }
1541
- // Interaction
1542
- this.eventMode = 'static';
1543
- this.cursor = 'pointer';
1544
- this.on('pointerover', this._onPointerOver, this);
1545
- this.on('pointerout', this._onPointerOut, this);
1546
- this.on('pointerdown', this._onPointerDown, this);
1547
- this.on('pointerup', this._onPointerUp, this);
1548
- this.on('pointerupoutside', this._onPointerUpOutside, this);
1549
- if (config.disabled) {
1550
- this.enabled = false;
1551
- }
1552
- }
1553
- /** Current button state */
1554
- get state() {
1555
- return this._state;
1556
- }
1557
- /** Enable the button */
1558
- enable() {
1559
- this.enabled = true;
1560
- }
1561
- /** Disable the button */
1562
- disable() {
1563
- this.enabled = false;
1564
- }
1565
- /** Whether the button is enabled */
1566
- get enabled() {
1567
- return this._enabled;
1568
- }
1569
- set enabled(value) {
1570
- this._enabled = value;
1571
- this.cursor = value ? 'pointer' : 'default';
1572
- this.eventMode = value ? 'static' : 'none';
1573
- this._setState(value ? 'default' : 'disabled');
1574
- }
1575
- /** Whether the button is disabled */
1576
- get disabled() {
1577
- return !this._enabled;
1578
- }
1579
- /** Update button text */
1580
- set text(value) {
1581
- if (this._textObj) {
1582
- this._textObj.text = value;
1583
- }
1584
- }
1585
- // ─── View building ──────────────────────────────────
1586
- _buildViews(config) {
1587
- const colorMap = { ...DEFAULT_COLORS, ...config.colors };
1588
- const { width, height, borderRadius } = this._config;
1589
- const stateViews = {
1590
- default: config.defaultView,
1591
- hover: config.hoverView,
1592
- pressed: config.pressedView,
1593
- disabled: config.disabledView,
1594
- };
1595
- const states = ['default', 'hover', 'pressed', 'disabled'];
1596
- for (const state of states) {
1597
- const customView = resolveView(stateViews[state]);
1598
- const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
1599
- view.visible = state === 'default';
1600
- this._views.set(state, view);
1601
- this.addChild(view);
1602
- }
1603
- }
1604
- _rebuildViews() {
1605
- for (const [, view] of this._views) {
1606
- this.removeChild(view);
1607
- view.destroy();
1608
- }
1609
- this._views.clear();
1610
- this._buildViews(this._config);
1611
- // Re-insert views before text
1612
- if (this._textObj && this._textObj.parent === this) {
1613
- this.setChildIndex(this._textObj, this.children.length - 1);
1614
- }
1615
- }
1616
- // ─── State management ───────────────────────────────
1617
- _setState(state) {
1618
- if (this._state === state)
1619
- return;
1620
- this._state = state;
1621
- for (const [s, view] of this._views) {
1622
- view.visible = s === state;
1623
- }
1624
- }
1625
- _onPointerOver() {
1626
- if (!this._enabled)
1627
- return;
1628
- this._setState('hover');
1629
- Tween.killTweensOf(this);
1630
- Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
1631
- }
1632
- _onPointerOut() {
1633
- if (!this._enabled)
1634
- return;
1635
- this._setState('default');
1636
- Tween.killTweensOf(this);
1637
- Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
1638
- }
1639
- _onPointerDown() {
1640
- if (!this._enabled)
1641
- return;
1642
- this._setState('pressed');
1643
- Tween.killTweensOf(this);
1644
- const s = this._config.pressScale;
1645
- Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
1646
- }
1647
- _onPointerUp() {
1648
- if (!this._enabled)
1649
- return;
1650
- this._setState('hover');
1651
- Tween.killTweensOf(this);
1652
- Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
1653
- this.onPress?.();
1654
- }
1655
- _onPointerUpOutside() {
1656
- if (!this._enabled)
1657
- return;
1658
- this._setState('default');
1659
- Tween.killTweensOf(this);
1660
- Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
1661
- }
1662
- /** React reconciler update hook */
1663
- updateConfig(changed) {
1664
- if ('text' in changed && this._textObj)
1665
- this._textObj.text = changed.text;
1666
- if ('disabled' in changed)
1667
- this.enabled = !changed.disabled;
1668
- if ('onPress' in changed)
1669
- this.onPress = changed.onPress;
1670
- const structural = [
1671
- 'colors', 'width', 'height', 'borderRadius', 'textStyle',
1672
- 'defaultView', 'hoverView', 'pressedView', 'disabledView',
1673
- ];
1674
- const needsRebuild = structural.some((k) => k in changed);
1675
- if (needsRebuild) {
1676
- Object.assign(this._config, changed);
1677
- this._rebuildViews();
1678
- }
1679
- }
1680
- destroy(options) {
1681
- Tween.killTweensOf(this);
1682
- this.off('pointerover', this._onPointerOver, this);
1683
- this.off('pointerout', this._onPointerOut, this);
1684
- this.off('pointerdown', this._onPointerDown, this);
1685
- this.off('pointerup', this._onPointerUp, this);
1686
- this.off('pointerupoutside', this._onPointerUpOutside, this);
1687
- this._views.clear();
1688
- this._textObj = null;
1689
- super.destroy(options);
1690
- }
1691
- }
1692
-
1693
- /**
1694
- * Horizontal progress bar with optional custom track/fill views.
1695
- *
1696
- * Supports asset-based skinning: provide `trackView` and/or `fillView`
1697
- * as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
1698
- * Falls back to colored Graphics when no custom views are provided.
1699
- *
1700
- * @example
1701
- * ```ts
1702
- * // Graphics-based (quick prototyping)
1703
- * const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
1704
- * bar.progress = 0.5;
1705
- *
1706
- * // Asset-based (production art)
1707
- * const bar = new ProgressBar({
1708
- * width: 300, height: 20,
1709
- * trackView: 'bar-track',
1710
- * fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
1711
- * });
1712
- * bar.progress = 0.75;
1713
- * ```
1714
- */
1715
- class ProgressBar extends pixi_js.Container {
1716
- __uiComponent = true;
1717
- _track;
1718
- _fill;
1719
- _fillMask;
1720
- _borderGfx;
1721
- _config;
1722
- _progress = 0;
1723
- _displayedProgress = 0;
1724
- constructor(config = {}) {
1725
- super();
1726
- this._config = {
1727
- width: config.width ?? 300,
1728
- height: config.height ?? 16,
1729
- borderRadius: config.borderRadius ?? 8,
1730
- fillColor: config.fillColor ?? 0xffd700,
1731
- trackColor: config.trackColor ?? 0x333333,
1732
- borderColor: config.borderColor ?? 0x555555,
1733
- borderWidth: config.borderWidth ?? 1,
1734
- animated: config.animated ?? true,
1735
- animationSpeed: config.animationSpeed ?? 0.1,
1736
- };
1737
- const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
1738
- // Track background — custom view or Graphics
1739
- const customTrack = resolveView(config.trackView);
1740
- if (customTrack) {
1741
- customTrack.width = width;
1742
- customTrack.height = height;
1743
- this._track = customTrack;
1744
- }
1745
- else {
1746
- const g = new pixi_js.Graphics();
1747
- g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
1748
- this._track = g;
1749
- }
1750
- this.addChild(this._track);
1751
- // Fill bar — custom view or Graphics
1752
- const customFill = resolveView(config.fillView);
1753
- if (customFill) {
1754
- customFill.x = borderWidth;
1755
- customFill.y = borderWidth;
1756
- customFill.width = width - borderWidth * 2;
1757
- customFill.height = height - borderWidth * 2;
1758
- this._fill = customFill;
1759
- }
1760
- else {
1761
- const g = new pixi_js.Graphics();
1762
- g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
1763
- this._fill = g;
1764
- }
1765
- this.addChild(this._fill);
1766
- // Mask for the fill (controls visible width)
1767
- this._fillMask = new pixi_js.Graphics();
1768
- this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
1769
- this.addChild(this._fillMask);
1770
- this._fill.mask = this._fillMask;
1771
- // Border overlay
1772
- this._borderGfx = new pixi_js.Graphics();
1773
- if (borderColor !== undefined && borderWidth > 0) {
1774
- this._borderGfx
1775
- .roundRect(0, 0, width, height, borderRadius)
1776
- .stroke({ color: borderColor, width: borderWidth });
1777
- }
1778
- this.addChild(this._borderGfx);
1779
- }
1780
- /** Get/set progress (0..1) */
1781
- get progress() {
1782
- return this._progress;
1783
- }
1784
- set progress(value) {
1785
- this._progress = Math.max(0, Math.min(1, value));
1786
- if (!this._config.animated) {
1787
- this._displayedProgress = this._progress;
1788
- this.updateMask();
1789
- }
1790
- }
1791
- /**
1792
- * Call each frame if animated is true.
1793
- */
1794
- update(_dt) {
1795
- if (!this._config.animated)
1796
- return;
1797
- if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
1798
- this._displayedProgress = this._progress;
1799
- this.updateMask();
1800
- return;
1801
- }
1802
- this._displayedProgress +=
1803
- (this._progress - this._displayedProgress) * this._config.animationSpeed;
1804
- this.updateMask();
1805
- }
1806
- /** React reconciler update hook */
1807
- updateConfig(changed) {
1808
- if ('progress' in changed)
1809
- this.progress = changed.progress;
1810
- if ('animated' in changed)
1811
- this._config.animated = changed.animated;
1812
- if ('animationSpeed' in changed)
1813
- this._config.animationSpeed = changed.animationSpeed;
1814
- }
1815
- updateMask() {
1816
- const w = this._config.width * this._displayedProgress;
1817
- this._fillMask.clear();
1818
- this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
1819
- }
1820
- }
1821
-
1822
- /**
1823
- * Enhanced text label with auto-fit scaling and currency formatting.
1824
- *
1825
- * @example
1826
- * ```ts
1827
- * const label = new Label({
1828
- * text: 'BALANCE',
1829
- * style: { fontSize: 24, fill: 0xffd700 },
1830
- * maxWidth: 200,
1831
- * autoFit: true,
1832
- * });
1833
- * ```
1834
- */
1835
- class Label extends pixi_js.Container {
1836
- __uiComponent = true;
1837
- _text;
1838
- _maxWidth;
1839
- _autoFit;
1840
- constructor(config = {}) {
1841
- super();
1842
- this._maxWidth = config.maxWidth ?? Infinity;
1843
- this._autoFit = config.autoFit ?? false;
1844
- this._text = new pixi_js.Text({
1845
- text: config.text ?? '',
1846
- style: {
1847
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1848
- fontSize: 24,
1849
- fill: 0xffffff,
1850
- ...config.style,
1851
- },
1852
- });
1853
- this._text.anchor.set(0.5);
1854
- this.addChild(this._text);
1855
- this.fitText();
1856
- }
1857
- /** Get/set the displayed text */
1858
- get text() {
1859
- return this._text.text;
1860
- }
1861
- set text(value) {
1862
- this._text.text = value;
1863
- this.fitText();
1864
- }
1865
- /** Get/set the text style */
1866
- get style() {
1867
- return this._text.style;
1868
- }
1869
- /** Set max width constraint */
1870
- set maxWidth(value) {
1871
- this._maxWidth = value;
1872
- this.fitText();
1873
- }
1874
- /**
1875
- * Format and display a number as currency.
1876
- *
1877
- * @param amount - The numeric amount
1878
- * @param currency - Currency code (e.g., 'USD', 'EUR')
1879
- * @param locale - Locale string (default: 'en-US')
1880
- */
1881
- setCurrency(amount, currency, locale = 'en-US') {
1882
- try {
1883
- this.text = new Intl.NumberFormat(locale, {
1884
- style: 'currency',
1885
- currency,
1886
- minimumFractionDigits: 2,
1887
- maximumFractionDigits: 2,
1888
- }).format(amount);
1889
- }
1890
- catch {
1891
- this.text = `${amount.toFixed(2)} ${currency}`;
1892
- }
1893
- }
1894
- /**
1895
- * Format a number with thousands separators.
1896
- */
1897
- setNumber(value, decimals = 0, locale = 'en-US') {
1898
- this.text = new Intl.NumberFormat(locale, {
1899
- minimumFractionDigits: decimals,
1900
- maximumFractionDigits: decimals,
1901
- }).format(value);
1902
- }
1903
- /** React reconciler update hook */
1904
- updateConfig(changed) {
1905
- if ('text' in changed)
1906
- this.text = changed.text;
1907
- if ('maxWidth' in changed)
1908
- this.maxWidth = changed.maxWidth;
1909
- if ('autoFit' in changed) {
1910
- this._autoFit = changed.autoFit;
1911
- this.fitText();
1912
- }
1913
- if ('style' in changed && typeof changed.style === 'object') {
1914
- Object.assign(this._text.style, changed.style);
1915
- this.fitText();
1916
- }
1917
- }
1918
- fitText() {
1919
- if (!this._autoFit || this._maxWidth === Infinity)
1920
- return;
1921
- this._text.scale.set(1);
1922
- if (this._text.width > this._maxWidth) {
1923
- const scale = this._maxWidth / this._text.width;
1924
- this._text.scale.set(scale);
1925
- }
1926
- }
1927
- }
1928
-
1929
- /**
1930
- * Two-row text cell with a muted caption above and a prominent value below —
1931
- * the archetypal casino UI pattern (BALANCE/€500, BET/€1, WIN/€0.00).
1932
- *
1933
- * Extends FlexContainer, so it participates in parent flex layouts directly
1934
- * (you can pass `flexGrow`, `alignSelf`, etc. when it's a child of another
1935
- * FlexContainer) and auto-sizes to its contents when no explicit size is set.
1936
- *
1937
- * @example
1938
- * ```tsx
1939
- * <labelValue
1940
- * label="BALANCE"
1941
- * value={`€${balance.toFixed(2)}`}
1942
- * labelStyle={{ fontSize: 12, fill: 0x888888 }}
1943
- * valueStyle={{ fontSize: 22, fill: 0xffffff, fontWeight: 'bold' }}
1944
- * gap={6}
1945
- * align="center"
1946
- * />
1947
- * ```
1948
- */
1949
- class LabelValue extends FlexContainer {
1950
- _labelEl;
1951
- _valueEl;
1952
- _valueMaxWidth;
1953
- constructor(config = {}) {
1954
- super({
1955
- direction: 'column',
1956
- alignItems: toAlignItems(config.align ?? 'center'),
1957
- gap: config.gap ?? 4,
1958
- padding: config.padding,
1959
- });
1960
- this._valueMaxWidth = config.maxWidth ?? Infinity;
1961
- this._labelEl = new Label({
1962
- text: config.label ?? '',
1963
- style: config.labelStyle,
1964
- });
1965
- this._valueEl = new Label({
1966
- text: config.value ?? '',
1967
- style: config.valueStyle,
1968
- maxWidth: config.maxWidth,
1969
- autoFit: config.maxWidth !== undefined,
1970
- });
1971
- this.addFlexChild(this._labelEl);
1972
- this.addFlexChild(this._valueEl);
1973
- }
1974
- /** Get the caption Label (for advanced styling / animation) */
1975
- get labelElement() {
1976
- return this._labelEl;
1977
- }
1978
- /** Get the value Label */
1979
- get valueElement() {
1980
- return this._valueEl;
1981
- }
1982
- /** Update caption text */
1983
- setLabel(text) {
1984
- this._labelEl.text = text;
1985
- this.updateLayout();
1986
- }
1987
- /** Update value text */
1988
- setValue(text) {
1989
- this._valueEl.text = text;
1990
- this.updateLayout();
1991
- }
1992
- /** React reconciler update hook */
1993
- updateConfig(changed) {
1994
- if ('label' in changed)
1995
- this._labelEl.text = changed.label ?? '';
1996
- if ('value' in changed)
1997
- this._valueEl.text = changed.value ?? '';
1998
- if ('labelStyle' in changed && typeof changed.labelStyle === 'object') {
1999
- Object.assign(this._labelEl.style, changed.labelStyle);
2000
- }
2001
- if ('valueStyle' in changed && typeof changed.valueStyle === 'object') {
2002
- Object.assign(this._valueEl.style, changed.valueStyle);
2003
- }
2004
- if ('maxWidth' in changed) {
2005
- this._valueMaxWidth = changed.maxWidth ?? Infinity;
2006
- this._valueEl.maxWidth = this._valueMaxWidth;
2007
- }
2008
- if ('gap' in changed)
2009
- this.setGap(changed.gap);
2010
- if ('align' in changed)
2011
- this.setAlignItems(toAlignItems(changed.align));
2012
- // Forward remaining FlexContainer props (e.g. padding, width, height)
2013
- super.updateConfig(changed);
2014
- }
2015
- }
2016
- function toAlignItems(align) {
2017
- return align;
2018
- }
2019
-
2020
- /**
2021
- * Background panel with optional flexbox content layout.
2022
- *
2023
- * Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
2024
- * Children added via `addContent()` participate in flex layout automatically.
2025
- *
2026
- * @example
2027
- * ```ts
2028
- * // Simple colored panel
2029
- * const panel = new Panel({ width: 400, height: 300, backgroundColor: 0x222222, borderRadius: 12 });
2030
- *
2031
- * // 9-slice panel (texture-based)
2032
- * const panel = new Panel({
2033
- * nineSliceTexture: 'panel-bg',
2034
- * nineSliceBorders: [20, 20, 20, 20],
2035
- * width: 400, height: 300,
2036
- * });
2037
- * ```
2038
- */
2039
- class Panel extends pixi_js.Container {
2040
- __uiComponent = true;
2041
- _bg;
2042
- _content;
2043
- _internalSetup = true;
2044
- _panelConfig;
2045
- constructor(config = {}) {
2046
- super();
2047
- const resolvedConfig = {
2048
- width: config.width ?? 400,
2049
- height: config.height ?? 300,
2050
- padding: config.padding ?? 16,
2051
- backgroundAlpha: config.backgroundAlpha ?? 1,
2052
- ...config,
2053
- };
2054
- this._panelConfig = resolvedConfig;
2055
- // Create background
2056
- if (config.nineSliceTexture) {
2057
- const texture = typeof config.nineSliceTexture === 'string'
2058
- ? pixi_js.Texture.from(config.nineSliceTexture)
2059
- : config.nineSliceTexture;
2060
- const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
2061
- const nineSlice = new pixi_js.NineSliceSprite({
2062
- texture,
2063
- leftWidth: left,
2064
- topHeight: top,
2065
- rightWidth: right,
2066
- bottomHeight: bottom,
2067
- });
2068
- nineSlice.width = resolvedConfig.width;
2069
- nineSlice.height = resolvedConfig.height;
2070
- nineSlice.alpha = resolvedConfig.backgroundAlpha;
2071
- this._bg = nineSlice;
2072
- }
2073
- else {
2074
- const g = new pixi_js.Graphics();
2075
- const bgColor = config.backgroundColor ?? 0x1a1a2e;
2076
- const radius = config.borderRadius ?? 0;
2077
- g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
2078
- if (config.borderColor !== undefined && config.borderWidth) {
2079
- g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
2080
- .stroke({ color: config.borderColor, width: config.borderWidth });
2081
- }
2082
- g.alpha = resolvedConfig.backgroundAlpha;
2083
- this._bg = g;
2084
- }
2085
- this.addChild(this._bg);
2086
- // Create content flex container
2087
- this._content = new FlexContainer({
2088
- ...config.layout,
2089
- direction: config.layout?.direction ?? 'column',
2090
- justifyContent: config.layout?.justifyContent ?? 'start',
2091
- alignItems: config.layout?.alignItems ?? 'start',
2092
- gap: config.layout?.gap ?? 0,
2093
- padding: resolvedConfig.padding,
2094
- width: resolvedConfig.width,
2095
- height: resolvedConfig.height,
2096
- });
2097
- this.addChild(this._content);
2098
- this._internalSetup = false;
2099
- }
2100
- /** Access the content flex container — add children here for layout */
2101
- get content() {
2102
- return this._content;
2103
- }
2104
- /** Suspend content layout recalculation. Call resumeLayout() to flush. */
2105
- suspendLayout() {
2106
- this._content.suspendLayout();
2107
- }
2108
- /** Resume content layout and flush if dirty. */
2109
- resumeLayout() {
2110
- this._content.resumeLayout();
2111
- }
2112
- /** Convenience: add a child to the content layout */
2113
- addContent(child) {
2114
- this._content.addFlexChild(child);
2115
- this._content.updateLayout();
2116
- return this;
2117
- }
2118
- /** Resize the panel */
2119
- setSize(width, height) {
2120
- this._panelConfig.width = width;
2121
- this._panelConfig.height = height;
2122
- // Resize background
2123
- if (this._bg instanceof pixi_js.NineSliceSprite) {
2124
- this._bg.width = width;
2125
- this._bg.height = height;
2126
- }
2127
- else if (this._bg instanceof pixi_js.Graphics) {
2128
- const radius = this._panelConfig.borderRadius ?? 0;
2129
- const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
2130
- this._bg.clear();
2131
- this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
2132
- if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
2133
- this._bg.roundRect(0, 0, width, height, radius)
2134
- .stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
2135
- }
2136
- this._bg.alpha = this._panelConfig.backgroundAlpha;
2137
- }
2138
- this._content.resize(width, height);
2139
- }
2140
- /**
2141
- * Override addChild so external children are routed to content FlexContainer.
2142
- * Enables `<panel><label /><button /></panel>` in React JSX.
2143
- */
2144
- addChild(...children) {
2145
- if (this._internalSetup) {
2146
- return super.addChild(...children);
2147
- }
2148
- for (const child of children) {
2149
- this._content.addFlexChild(child);
2150
- }
2151
- this._content.updateLayout();
2152
- return children[0];
2153
- }
2154
- removeChild(...children) {
2155
- if (this._internalSetup) {
2156
- return super.removeChild(...children);
2157
- }
2158
- for (const child of children) {
2159
- this._content.removeFlexChild(child);
2160
- }
2161
- return children[0];
2162
- }
2163
- /** React reconciler update hook */
2164
- updateConfig(changed) {
2165
- if ('width' in changed || 'height' in changed) {
2166
- this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
2167
- }
2168
- if ('backgroundAlpha' in changed) {
2169
- this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
2170
- this._bg.alpha = changed.backgroundAlpha;
2171
- }
2172
- }
2173
- destroy(options) {
2174
- super.destroy(options);
2175
- }
2176
- }
2177
-
2178
- /**
2179
- * Reactive balance display component.
2180
- *
2181
- * Automatically formats currency and can animate value changes
2182
- * with a smooth countup/countdown effect using engine Tween.
2183
- *
2184
- * @example
2185
- * ```ts
2186
- * const balance = new BalanceDisplay({ currency: 'USD', animated: true });
2187
- * balance.setValue(1000);
2188
- *
2189
- * // Wire to SDK
2190
- * sdk.on('balanceUpdate', ({ balance: val }) => balance.setValue(val));
2191
- * ```
2192
- */
2193
- class BalanceDisplay extends pixi_js.Container {
2194
- __uiComponent = true;
2195
- _prefixLabel = null;
2196
- _valueLabel;
2197
- _config;
2198
- _currentValue = 0;
2199
- _displayedValue = 0;
2200
- /** Internal target for Tween animation */
2201
- _tweenTarget = { value: 0 };
2202
- constructor(config = {}) {
2203
- super();
2204
- this._config = {
2205
- currency: config.currency ?? 'USD',
2206
- locale: config.locale ?? 'en-US',
2207
- animated: config.animated ?? true,
2208
- animationDuration: config.animationDuration ?? 500,
2209
- };
2210
- // Prefix label
2211
- if (config.prefix) {
2212
- this._prefixLabel = new Label({
2213
- text: config.prefix,
2214
- style: {
2215
- fontSize: 16,
2216
- fill: 0xaaaaaa,
2217
- ...config.style,
2218
- },
2219
- });
2220
- this.addChild(this._prefixLabel);
2221
- }
2222
- // Value label
2223
- this._valueLabel = new Label({
2224
- text: '0.00',
2225
- style: {
2226
- fontSize: 28,
2227
- fontWeight: 'bold',
2228
- fill: 0xffffff,
2229
- ...config.style,
2230
- },
2231
- maxWidth: config.maxWidth,
2232
- autoFit: !!config.maxWidth,
2233
- });
2234
- this.addChild(this._valueLabel);
2235
- this.layoutLabels();
2236
- }
2237
- /** Current displayed value */
2238
- get value() {
2239
- return this._currentValue;
2240
- }
2241
- /**
2242
- * Set the balance value. If animated, smoothly counts to the new value.
2243
- */
2244
- setValue(value) {
2245
- const oldValue = this._currentValue;
2246
- this._currentValue = value;
2247
- if (this._config.animated && oldValue !== value) {
2248
- this.animateValue(oldValue, value);
2249
- }
2250
- else {
2251
- this._displayedValue = value;
2252
- this.updateDisplay();
2253
- }
2254
- }
2255
- /**
2256
- * Set the currency code.
2257
- */
2258
- setCurrency(currency) {
2259
- this._config.currency = currency;
2260
- this.updateDisplay();
2261
- }
2262
- animateValue(from, to) {
2263
- // Cancel any running animation
2264
- Tween.killTweensOf(this._tweenTarget);
2265
- this._tweenTarget.value = from;
2266
- Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
2267
- this._displayedValue = this._tweenTarget.value;
2268
- this.updateDisplay();
2269
- });
2270
- }
2271
- updateDisplay() {
2272
- this._valueLabel.setCurrency(this._displayedValue, this._config.currency, this._config.locale);
2273
- }
2274
- layoutLabels() {
2275
- if (this._prefixLabel) {
2276
- this._prefixLabel.y = -14;
2277
- this._valueLabel.y = 14;
2278
- }
2279
- }
2280
- /** React reconciler update hook */
2281
- updateConfig(changed) {
2282
- if ('value' in changed)
2283
- this.setValue(changed.value);
2284
- if ('currency' in changed)
2285
- this.setCurrency(changed.currency);
2286
- }
2287
- destroy(options) {
2288
- Tween.killTweensOf(this._tweenTarget);
2289
- super.destroy(options);
2290
- }
2291
- }
2292
-
2293
- /**
2294
- * Win amount display with countup animation.
2295
- *
2296
- * Shows a dramatic countup from 0 to the win amount, with optional
2297
- * scale pop effect — typical of slot games. Uses engine Tween system.
2298
- *
2299
- * @example
2300
- * ```ts
2301
- * const winDisplay = new WinDisplay({ currency: 'USD' });
2302
- * scene.container.addChild(winDisplay);
2303
- * await winDisplay.showWin(150.50); // countup animation
2304
- * winDisplay.hide();
2305
- * ```
2306
- */
2307
- class WinDisplay extends pixi_js.Container {
2308
- __uiComponent = true;
2309
- _label;
2310
- _config;
2311
- /** Internal target for Tween countup */
2312
- _tweenTarget = { value: 0 };
2313
- constructor(config = {}) {
2314
- super();
2315
- this._config = {
2316
- currency: config.currency ?? 'USD',
2317
- locale: config.locale ?? 'en-US',
2318
- countupDuration: config.countupDuration ?? 1500,
2319
- popScale: config.popScale ?? 1.2,
2320
- };
2321
- this._label = new Label({
2322
- text: '',
2323
- style: {
2324
- fontSize: 48,
2325
- fontWeight: 'bold',
2326
- fill: 0xffd700,
2327
- stroke: { color: 0x000000, width: 3 },
2328
- ...config.style,
2329
- },
2330
- });
2331
- this.addChild(this._label);
2332
- this.visible = false;
2333
- }
2334
- /**
2335
- * Show a win with countup animation.
2336
- *
2337
- * @param amount - Win amount
2338
- * @returns Promise that resolves when the animation completes
2339
- */
2340
- async showWin(amount) {
2341
- this.visible = true;
2342
- this.alpha = 1;
2343
- // Cancel any running animation
2344
- Tween.killTweensOf(this._tweenTarget);
2345
- Tween.killTweensOf(this);
2346
- // Setup countup
2347
- this._tweenTarget.value = 0;
2348
- this.scale.set(0.5);
2349
- // Scale pop animation
2350
- const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
2351
- // Countup animation
2352
- const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
2353
- this.displayAmount(this._tweenTarget.value);
2354
- });
2355
- await Promise.all([scalePromise, countupPromise]);
2356
- // Ensure final value is exact
2357
- this.displayAmount(amount);
2358
- this.scale.set(1);
2359
- }
2360
- /**
2361
- * Skip the countup animation and show the final amount immediately.
2362
- */
2363
- skipCountup(amount) {
2364
- Tween.killTweensOf(this._tweenTarget);
2365
- Tween.killTweensOf(this);
2366
- this.displayAmount(amount);
2367
- this.scale.set(1);
2368
- }
2369
- /**
2370
- * Hide the win display.
2371
- */
2372
- hide() {
2373
- Tween.killTweensOf(this._tweenTarget);
2374
- Tween.killTweensOf(this);
2375
- this.visible = false;
2376
- this._label.text = '';
2377
- }
2378
- displayAmount(amount) {
2379
- this._label.setCurrency(amount, this._config.currency, this._config.locale);
2380
- }
2381
- /** React reconciler update hook */
2382
- updateConfig(changed) {
2383
- if ('currency' in changed)
2384
- this._config.currency = changed.currency;
2385
- if ('locale' in changed)
2386
- this._config.locale = changed.locale;
2387
- }
2388
- destroy(options) {
2389
- Tween.killTweensOf(this._tweenTarget);
2390
- Tween.killTweensOf(this);
2391
- super.destroy(options);
2392
- }
2393
- }
2394
-
2395
- /**
2396
- * Modal overlay component.
2397
- * Shows content on top of a dark overlay with enter/exit animations.
2398
- *
2399
- * Content is automatically centered via position calculations.
2400
- *
2401
- * @example
2402
- * ```ts
2403
- * const modal = new Modal({ closeOnOverlay: true });
2404
- * modal.content.addChild(settingsPanel);
2405
- * modal.onClose = () => console.log('Closed');
2406
- * await modal.show(1920, 1080);
2407
- * ```
2408
- */
2409
- class Modal extends pixi_js.Container {
2410
- __uiComponent = true;
2411
- _overlay;
2412
- _contentContainer;
2413
- _config;
2414
- _showing = false;
2415
- _internalSetup = true;
2416
- /** Called when the modal is closed */
2417
- onClose;
2418
- constructor(config = {}) {
2419
- super();
2420
- this._config = {
2421
- overlayColor: config.overlayColor ?? 0x000000,
2422
- overlayAlpha: config.overlayAlpha ?? 0.7,
2423
- closeOnOverlay: config.closeOnOverlay ?? true,
2424
- animationDuration: config.animationDuration ?? 300,
2425
- };
2426
- // Overlay
2427
- this._overlay = new pixi_js.Graphics();
2428
- this._overlay.eventMode = 'static';
2429
- this._overlay.on('pointertap', () => {
2430
- if (this._config.closeOnOverlay)
2431
- this.hide();
2432
- });
2433
- this.addChild(this._overlay);
2434
- // Content container
2435
- this._contentContainer = new pixi_js.Container();
2436
- this.addChild(this._contentContainer);
2437
- this.visible = false;
2438
- this._internalSetup = false;
2439
- }
2440
- /** Content container — add your UI here */
2441
- get content() {
2442
- return this._contentContainer;
2443
- }
2444
- /**
2445
- * Override addChild so external children are routed to _contentContainer.
2446
- * Enables `<modal><flexContainer>...</flexContainer></modal>` in React JSX.
2447
- */
2448
- addChild(...children) {
2449
- if (this._internalSetup) {
2450
- return super.addChild(...children);
2451
- }
2452
- for (const child of children) {
2453
- this._contentContainer.addChild(child);
2454
- }
2455
- return children[0];
2456
- }
2457
- removeChild(...children) {
2458
- if (this._internalSetup) {
2459
- return super.removeChild(...children);
2460
- }
2461
- for (const child of children) {
2462
- this._contentContainer.removeChild(child);
2463
- }
2464
- return children[0];
2465
- }
2466
- /** Whether the modal is currently showing */
2467
- get isShowing() {
2468
- return this._showing;
2469
- }
2470
- /**
2471
- * Show the modal with animation.
2472
- */
2473
- async show(viewWidth, viewHeight) {
2474
- this._showing = true;
2475
- this.visible = true;
2476
- // Draw overlay to cover full screen
2477
- this._overlay.clear();
2478
- this._overlay.rect(0, 0, viewWidth, viewHeight).fill(this._config.overlayColor);
2479
- this._overlay.alpha = 0;
2480
- // Center content
2481
- this._contentContainer.x = viewWidth / 2;
2482
- this._contentContainer.y = viewHeight / 2;
2483
- this._contentContainer.alpha = 0;
2484
- this._contentContainer.scale.set(0.8);
2485
- // Animate in
2486
- await Promise.all([
2487
- Tween.to(this._overlay, { alpha: this._config.overlayAlpha }, this._config.animationDuration, Easing.easeOutCubic),
2488
- Tween.to(this._contentContainer, { alpha: 1, 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutBack),
2489
- ]);
2490
- }
2491
- /**
2492
- * Hide the modal with animation.
2493
- */
2494
- async hide() {
2495
- if (!this._showing)
2496
- return;
2497
- await Promise.all([
2498
- Tween.to(this._overlay, { alpha: 0 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
2499
- Tween.to(this._contentContainer, { alpha: 0, 'scale.x': 0.8, 'scale.y': 0.8 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
2500
- ]);
2501
- this.visible = false;
2502
- this._showing = false;
2503
- this.onClose?.();
2504
- }
2505
- /** React reconciler update hook */
2506
- updateConfig(changed) {
2507
- if ('overlayAlpha' in changed)
2508
- this._config.overlayAlpha = changed.overlayAlpha;
2509
- if ('closeOnOverlay' in changed)
2510
- this._config.closeOnOverlay = changed.closeOnOverlay;
2511
- if ('animationDuration' in changed)
2512
- this._config.animationDuration = changed.animationDuration;
2513
- if ('onClose' in changed)
2514
- this.onClose = changed.onClose;
2515
- }
2516
- }
2517
-
2518
- const TOAST_COLORS = {
2519
- info: 0x3498db,
2520
- success: 0x27ae60,
2521
- warning: 0xf39c12,
2522
- error: 0xe74c3c,
2523
- };
2524
- /**
2525
- * Toast notification component for displaying transient messages.
2526
- *
2527
- * @example
2528
- * ```ts
2529
- * const toast = new Toast();
2530
- * scene.container.addChild(toast);
2531
- * await toast.show('Connection lost', 'error', 1920, 1080);
2532
- * ```
2533
- */
2534
- class Toast extends pixi_js.Container {
2535
- __uiComponent = true;
2536
- _bg;
2537
- _customBg;
2538
- _text;
2539
- _config;
2540
- _dismissPending = false;
2541
- constructor(config = {}) {
2542
- super();
2543
- this._config = {
2544
- duration: config.duration ?? 3000,
2545
- bottomOffset: config.bottomOffset ?? 60,
2546
- };
2547
- const customBg = resolveView(config.backgroundView);
2548
- this._customBg = !!customBg;
2549
- this._bg = customBg ?? new pixi_js.Graphics();
2550
- this.addChild(this._bg);
2551
- this._text = new pixi_js.Text({
2552
- text: '',
2553
- style: {
2554
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2555
- fontSize: 16,
2556
- fill: 0xffffff,
2557
- },
2558
- });
2559
- this._text.anchor.set(0.5);
2560
- this.addChild(this._text);
2561
- this.visible = false;
2562
- }
2563
- /**
2564
- * Show a toast message.
2565
- */
2566
- async show(message, type = 'info', viewWidth, viewHeight) {
2567
- // Cancel any pending dismiss
2568
- Tween.killTweensOf(this);
2569
- this._dismissPending = false;
2570
- this._text.text = message;
2571
- const padding = 20;
2572
- const width = Math.max(200, this._text.width + padding * 2);
2573
- const height = 44;
2574
- const radius = 8;
2575
- // Draw the background
2576
- if (this._customBg) {
2577
- this._bg.width = width;
2578
- this._bg.height = height;
2579
- this._bg.x = -width / 2;
2580
- this._bg.y = -height / 2;
2581
- }
2582
- else {
2583
- const g = this._bg;
2584
- g.clear();
2585
- g.roundRect(-width / 2, -height / 2, width, height, radius);
2586
- g.fill(TOAST_COLORS[type]);
2587
- }
2588
- // Position
2589
- if (viewWidth && viewHeight) {
2590
- this.x = viewWidth / 2;
2591
- this.y = viewHeight - this._config.bottomOffset;
2592
- }
2593
- this.visible = true;
2594
- this.alpha = 0;
2595
- this.y += 20;
2596
- await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
2597
- if (this._config.duration > 0) {
2598
- this._dismissPending = true;
2599
- await Tween.delay(this._config.duration);
2600
- if (this._dismissPending) {
2601
- this._dismissPending = false;
2602
- await this.dismiss();
2603
- }
2604
- }
2605
- }
2606
- /**
2607
- * Dismiss the toast.
2608
- */
2609
- async dismiss() {
2610
- if (!this.visible)
2611
- return;
2612
- this._dismissPending = false;
2613
- Tween.killTweensOf(this);
2614
- await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
2615
- this.visible = false;
2616
- }
2617
- /** React reconciler update hook */
2618
- updateConfig(changed) {
2619
- if ('duration' in changed)
2620
- this._config.duration = changed.duration;
2621
- if ('bottomOffset' in changed)
2622
- this._config.bottomOffset = changed.bottomOffset;
2623
- }
2624
- destroy(options) {
2625
- this._dismissPending = false;
2626
- Tween.killTweensOf(this);
2627
- super.destroy(options);
2628
- }
2629
- }
2630
-
2631
- // ─── Helpers ─────────────────────────────────────────────
2632
- function directionToFlex(direction) {
2633
- switch (direction) {
2634
- case 'horizontal': return { direction: 'row', wrap: false };
2635
- case 'vertical': return { direction: 'column', wrap: false };
2636
- case 'grid': return { direction: 'row', wrap: true };
2637
- case 'wrap': return { direction: 'row', wrap: true };
2638
- }
2639
- }
2640
- /**
2641
- * Responsive layout container powered by a lightweight built-in flex layout solver.
2642
- *
2643
- * Supports horizontal, vertical, grid, and wrap layout modes with
2644
- * alignment, padding, gap, and viewport-anchor positioning.
2645
- * Breakpoints allow different layouts for different screen sizes.
2646
- *
2647
- * @example
2648
- * ```ts
2649
- * const toolbar = new Layout({
2650
- * direction: 'horizontal',
2651
- * gap: 20,
2652
- * alignment: 'center',
2653
- * anchor: 'bottom-center',
2654
- * padding: 16,
2655
- * breakpoints: {
2656
- * 768: { direction: 'vertical', gap: 10 },
2657
- * },
2658
- * });
2659
- *
2660
- * toolbar.addItem(spinButton);
2661
- * toolbar.addItem(betLabel);
2662
- * scene.container.addChild(toolbar);
2663
- *
2664
- * toolbar.updateViewport(width, height);
2665
- * ```
2666
- */
2667
- class Layout extends pixi_js.Container {
2668
- __uiComponent = true;
2669
- _layoutConfig;
2670
- _padding;
2671
- _anchor;
2672
- _maxWidth;
2673
- _breakpoints;
2674
- _items = [];
2675
- _viewportWidth = 0;
2676
- _viewportHeight = 0;
2677
- _flex;
2678
- constructor(config = {}) {
2679
- super();
2680
- this._layoutConfig = {
2681
- direction: config.direction ?? 'vertical',
2682
- gap: config.gap ?? 0,
2683
- alignment: config.alignment ?? 'start',
2684
- autoLayout: config.autoLayout ?? true,
2685
- columns: config.columns ?? 2,
2686
- };
2687
- this._padding = config.padding ?? 0;
2688
- this._anchor = config.anchor ?? 'top-left';
2689
- this._maxWidth = config.maxWidth ?? Infinity;
2690
- this._breakpoints = config.breakpoints
2691
- ? Object.entries(config.breakpoints)
2692
- .map(([w, cfg]) => [Number(w), cfg])
2693
- .sort((a, b) => a[0] - b[0])
2694
- : [];
2695
- // Create internal FlexContainer
2696
- this._flex = new FlexContainer();
2697
- super.addChild(this._flex);
2698
- this.applyLayoutStyles();
2699
- }
2700
- /** Add an item to the layout */
2701
- addItem(child) {
2702
- this._items.push(child);
2703
- const flexConfig = this.buildFlexItemConfig(child);
2704
- this._flex.addFlexChild(child, flexConfig);
2705
- if (this._layoutConfig.autoLayout) {
2706
- this.applyLayoutStyles();
2707
- }
2708
- return this;
2709
- }
2710
- /** Remove an item from the layout */
2711
- removeItem(child) {
2712
- const idx = this._items.indexOf(child);
2713
- if (idx !== -1) {
2714
- this._items.splice(idx, 1);
2715
- this._flex.removeFlexChild(child);
2716
- }
2717
- return this;
2718
- }
2719
- /** Remove all items */
2720
- clearItems() {
2721
- this._flex.clearFlexChildren();
2722
- this._items.length = 0;
2723
- return this;
2724
- }
2725
- /** Get all layout items */
2726
- get items() {
2727
- return this._items;
2728
- }
2729
- /**
2730
- * Update the viewport size and recalculate layout.
2731
- * Should be called from `Scene.onResize()`.
2732
- */
2733
- updateViewport(width, height) {
2734
- this._viewportWidth = width;
2735
- this._viewportHeight = height;
2736
- this.applyLayoutStyles();
2737
- this.applyAnchor();
2738
- }
2739
- applyLayoutStyles() {
2740
- const effective = this.resolveConfig();
2741
- const direction = effective.direction ?? this._layoutConfig.direction;
2742
- const gap = effective.gap ?? this._layoutConfig.gap;
2743
- const alignment = effective.alignment ?? this._layoutConfig.alignment;
2744
- const padding = effective.padding ?? this._padding;
2745
- const maxWidth = effective.maxWidth ?? this._maxWidth;
2746
- const { direction: flexDir, wrap } = directionToFlex(direction);
2747
- this._flex.setDirection(flexDir);
2748
- this._flex.setJustifyContent('start');
2749
- this._flex.setAlignItems(alignment);
2750
- this._flex.setGap(gap);
2751
- this._flex.setPadding(padding);
2752
- // Wrap and maxWidth
2753
- if (wrap) {
2754
- this._flex._config.flexWrap = true;
2755
- if (direction === 'grid' && maxWidth < Infinity) {
2756
- this._flex._maxWidth = maxWidth;
2757
- }
2758
- if (maxWidth < Infinity) {
2759
- this._flex._maxWidth = maxWidth;
2760
- }
2761
- }
2762
- else {
2763
- this._flex._config.flexWrap = false;
2764
- }
2765
- // Update grid child widths
2766
- if (direction === 'grid') {
2767
- for (const item of this._items) {
2768
- const flexConfig = this.buildFlexItemConfig(item);
2769
- item._flexConfig = flexConfig;
2770
- }
2771
- }
2772
- // Set explicit size if we have viewport dimensions
2773
- if (this._viewportWidth > 0 && this._viewportHeight > 0) {
2774
- this._flex.resize(this._viewportWidth, this._viewportHeight);
2775
- }
2776
- else {
2777
- this._flex.updateLayout();
2778
- }
2779
- }
2780
- buildFlexItemConfig(_child) {
2781
- const effective = this.resolveConfig();
2782
- const direction = effective.direction ?? this._layoutConfig.direction;
2783
- const columns = effective.columns ?? this._layoutConfig.columns;
2784
- if (direction === 'grid' && columns > 0) {
2785
- // For grid, give each item a proportional width
2786
- // The actual pixel width will be computed during layout
2787
- return { flexGrow: 1 };
2788
- }
2789
- return undefined;
2790
- }
2791
- applyAnchor() {
2792
- const anchor = this.resolveConfig().anchor ?? this._anchor;
2793
- if (this._viewportWidth === 0 || this._viewportHeight === 0)
2794
- return;
2795
- const { width: contentW, height: contentH } = this._flex.getContentSize();
2796
- const vw = this._viewportWidth;
2797
- const vh = this._viewportHeight;
2798
- let anchorX = 0;
2799
- let anchorY = 0;
2800
- if (anchor.includes('left')) {
2801
- anchorX = 0;
2802
- }
2803
- else if (anchor.includes('right')) {
2804
- anchorX = vw - contentW;
2805
- }
2806
- else {
2807
- anchorX = (vw - contentW) / 2;
2808
- }
2809
- if (anchor.startsWith('top')) {
2810
- anchorY = 0;
2811
- }
2812
- else if (anchor.startsWith('bottom')) {
2813
- anchorY = vh - contentH;
2814
- }
2815
- else {
2816
- anchorY = (vh - contentH) / 2;
2817
- }
2818
- this.x = anchorX;
2819
- this.y = anchorY;
2820
- }
2821
- resolveConfig() {
2822
- if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
2823
- return {};
2824
- }
2825
- for (const [maxWidth, overrides] of this._breakpoints) {
2826
- if (this._viewportWidth <= maxWidth) {
2827
- return overrides;
2828
- }
2829
- }
2830
- return {};
2831
- }
2832
- /** React reconciler update hook */
2833
- updateConfig(changed) {
2834
- if ('direction' in changed)
2835
- this._layoutConfig.direction = changed.direction;
2836
- if ('gap' in changed)
2837
- this._layoutConfig.gap = changed.gap;
2838
- if ('alignment' in changed)
2839
- this._layoutConfig.alignment = changed.alignment;
2840
- if ('anchor' in changed)
2841
- this._anchor = changed.anchor;
2842
- if ('padding' in changed)
2843
- this._padding = changed.padding;
2844
- if ('columns' in changed)
2845
- this._layoutConfig.columns = changed.columns;
2846
- this.applyLayoutStyles();
2847
- if (this._viewportWidth > 0)
2848
- this.applyAnchor();
2849
- }
2850
- destroy(options) {
2851
- this._items.length = 0;
2852
- super.destroy(options);
2853
- }
2854
- }
2855
-
2856
- const DECELERATION = 0.95;
2857
- const MIN_VELOCITY = 0.5;
2858
- /**
2859
- * Scrollable container with touch/drag, mouse wheel, and inertia.
2860
- *
2861
- * @example
2862
- * ```ts
2863
- * const scroll = new ScrollContainer({
2864
- * width: 600,
2865
- * height: 400,
2866
- * direction: 'vertical',
2867
- * elementsMargin: 8,
2868
- * });
2869
- *
2870
- * for (let i = 0; i < 50; i++) {
2871
- * scroll.addItem(createRow(i));
2872
- * }
2873
- *
2874
- * scene.container.addChild(scroll);
2875
- * ```
2876
- */
2877
- class ScrollContainer extends pixi_js.Container {
2878
- __uiComponent = true;
2879
- _viewport;
2880
- _internalSetup = true;
2881
- _content;
2882
- _maskGfx;
2883
- _bg = null;
2884
- _scrollConfig;
2885
- _items = [];
2886
- // Scrollbar
2887
- _scrollbar = null;
2888
- _scrollbarConfig;
2889
- // Drag state
2890
- _dragging = false;
2891
- _dragStart = { x: 0, y: 0 };
2892
- _contentStart = { x: 0, y: 0 };
2893
- _velocity = { x: 0, y: 0 };
2894
- _lastDragPos = { x: 0, y: 0 };
2895
- _lastDragTime = 0;
2896
- _inertiaActive = false;
2897
- // Bound handlers for cleanup
2898
- _onTickBound = null;
2899
- _onWheelBound = null;
2900
- constructor(config) {
2901
- super();
2902
- this._viewport = { width: config.width, height: config.height };
2903
- this._scrollConfig = {
2904
- direction: config.direction ?? 'vertical',
2905
- elementsMargin: config.elementsMargin ?? 0,
2906
- padding: config.padding ?? 0,
2907
- borderRadius: config.borderRadius ?? 0,
2908
- disableEasing: config.disableEasing ?? false,
2909
- };
2910
- // Background
2911
- if (config.backgroundColor !== undefined) {
2912
- this._bg = new pixi_js.Graphics();
2913
- this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
2914
- .fill(config.backgroundColor);
2915
- this.addChild(this._bg);
2916
- }
2917
- // Mask
2918
- this._maskGfx = new pixi_js.Graphics();
2919
- this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
2920
- .fill(0xffffff);
2921
- this.addChild(this._maskGfx);
2922
- // Content container
2923
- this._content = new pixi_js.Container();
2924
- this._content.mask = this._maskGfx;
2925
- this.addChild(this._content);
2926
- // Interaction
2927
- this.eventMode = 'static';
2928
- this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
2929
- this.on('pointerdown', this._onPointerDown, this);
2930
- this.on('pointermove', this._onPointerMove, this);
2931
- this.on('pointerup', this._onPointerUp, this);
2932
- this.on('pointerupoutside', this._onPointerUp, this);
2933
- // Mouse wheel
2934
- this._onWheelBound = this._onWheel.bind(this);
2935
- // Scrollbar
2936
- const sbWidth = config.scrollbarWidth ?? 6;
2937
- const sbPadding = config.scrollbarPadding ?? 4;
2938
- this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
2939
- if (config.scrollbar) {
2940
- const customThumb = resolveView(config.thumbView);
2941
- if (customThumb) {
2942
- this._scrollbar = customThumb;
2943
- }
2944
- else {
2945
- const g = new pixi_js.Graphics();
2946
- g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
2947
- g.alpha = config.scrollbarAlpha ?? 0.5;
2948
- this._scrollbar = g;
2949
- }
2950
- this._scrollbar.visible = false;
2951
- super.addChild(this._scrollbar);
2952
- }
2953
- this._internalSetup = false;
2954
- }
2955
- /**
2956
- * Override addChild so external children are routed to scroll content.
2957
- * Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
2958
- */
2959
- addChild(...children) {
2960
- if (this._internalSetup) {
2961
- return super.addChild(...children);
2962
- }
2963
- for (const child of children) {
2964
- this.addItem(child);
2965
- }
2966
- return children[0];
2967
- }
2968
- removeChild(...children) {
2969
- if (this._internalSetup) {
2970
- return super.removeChild(...children);
2971
- }
2972
- for (const child of children) {
2973
- const idx = this._items.indexOf(child);
2974
- if (idx !== -1) {
2975
- this._items.splice(idx, 1);
2976
- this._content.removeChild(child);
2977
- }
2978
- }
2979
- this.layoutItems();
2980
- return children[0];
2981
- }
2982
- /** React reconciler update hook */
2983
- updateConfig(changed) {
2984
- if ('width' in changed || 'height' in changed) {
2985
- this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
2986
- }
2987
- }
2988
- /** Enable mouse wheel scrolling (call after adding to stage) */
2989
- enableWheel(canvas) {
2990
- if (this._onWheelBound) {
2991
- canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
2992
- }
2993
- }
2994
- /** Set scrollable content. Replaces any existing items. */
2995
- setContent(content) {
2996
- this.clearItems();
2997
- const children = [...content.children];
2998
- for (const child of children) {
2999
- this.addItem(child);
3000
- }
3001
- }
3002
- /** Add a single item */
3003
- addItem(child) {
3004
- this._items.push(child);
3005
- this._content.addChild(child);
3006
- this.layoutItems();
3007
- return this;
3008
- }
3009
- /** Remove all items */
3010
- clearItems() {
3011
- for (const item of this._items) {
3012
- this._content.removeChild(item);
3013
- }
3014
- this._items.length = 0;
3015
- }
3016
- /** Get items */
3017
- get items() {
3018
- return this._items;
3019
- }
3020
- /** Scroll to make a specific item index visible */
3021
- scrollToItem(index) {
3022
- if (index < 0 || index >= this._items.length)
3023
- return;
3024
- const item = this._items[index];
3025
- const isVert = this._scrollConfig.direction !== 'horizontal';
3026
- if (isVert) {
3027
- this._content.y = -item.y + this._scrollConfig.padding;
3028
- }
3029
- else {
3030
- this._content.x = -item.x + this._scrollConfig.padding;
3031
- }
3032
- this.clampScroll();
3033
- }
3034
- /** Current scroll position */
3035
- get scrollPosition() {
3036
- return { x: this._content.x, y: this._content.y };
3037
- }
3038
- /** Resize the scroll viewport */
3039
- setViewportSize(width, height) {
3040
- this._viewport.width = width;
3041
- this._viewport.height = height;
3042
- this._maskGfx.clear();
3043
- this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
3044
- if (this._bg) {
3045
- this._bg.clear();
3046
- this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
3047
- .fill(0xffffff); // color will be overridden if needed
3048
- }
3049
- this.clampScroll();
3050
- }
3051
- // ─── Layout ──────────────────────────────────────────
3052
- layoutItems() {
3053
- const { direction, elementsMargin, padding } = this._scrollConfig;
3054
- const isVert = direction !== 'horizontal';
3055
- let pos = padding;
3056
- for (const item of this._items) {
3057
- if (isVert) {
3058
- item.x = padding;
3059
- item.y = pos;
3060
- pos += item.height + elementsMargin;
3061
- }
3062
- else {
3063
- item.x = pos;
3064
- item.y = padding;
3065
- pos += item.width + elementsMargin;
3066
- }
3067
- }
3068
- }
3069
- // ─── Drag handling ───────────────────────────────────
3070
- _onPointerDown(e) {
3071
- this._dragging = true;
3072
- this._inertiaActive = false;
3073
- this._dragStart.x = e.globalX;
3074
- this._dragStart.y = e.globalY;
3075
- this._contentStart.x = this._content.x;
3076
- this._contentStart.y = this._content.y;
3077
- this._lastDragPos.x = e.globalX;
3078
- this._lastDragPos.y = e.globalY;
3079
- this._lastDragTime = Date.now();
3080
- this._velocity.x = 0;
3081
- this._velocity.y = 0;
3082
- this.stopInertia();
3083
- }
3084
- _onPointerMove(e) {
3085
- if (!this._dragging)
3086
- return;
3087
- const dx = e.globalX - this._dragStart.x;
3088
- const dy = e.globalY - this._dragStart.y;
3089
- const { direction } = this._scrollConfig;
3090
- if (direction !== 'horizontal') {
3091
- this._content.y = this._contentStart.y + dy;
3092
- }
3093
- if (direction !== 'vertical') {
3094
- this._content.x = this._contentStart.x + dx;
3095
- }
3096
- // Track velocity
3097
- const now = Date.now();
3098
- const dt = now - this._lastDragTime;
3099
- if (dt > 0) {
3100
- this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
3101
- this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
3102
- }
3103
- this._lastDragPos.x = e.globalX;
3104
- this._lastDragPos.y = e.globalY;
3105
- this._lastDragTime = now;
3106
- this.clampScroll();
3107
- }
3108
- _onPointerUp() {
3109
- if (!this._dragging)
3110
- return;
3111
- this._dragging = false;
3112
- if (!this._scrollConfig.disableEasing &&
3113
- (Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
3114
- this.startInertia();
3115
- }
3116
- }
3117
- // ─── Inertia ─────────────────────────────────────────
3118
- startInertia() {
3119
- this._inertiaActive = true;
3120
- this._onTickBound = this._inertiaTick.bind(this);
3121
- pixi_js.Ticker.shared.add(this._onTickBound);
3122
- }
3123
- stopInertia() {
3124
- if (this._onTickBound && this._inertiaActive) {
3125
- pixi_js.Ticker.shared.remove(this._onTickBound);
3126
- this._inertiaActive = false;
3127
- }
3128
- }
3129
- _inertiaTick() {
3130
- const { direction } = this._scrollConfig;
3131
- if (direction !== 'horizontal') {
3132
- this._content.y += this._velocity.y;
3133
- this._velocity.y *= DECELERATION;
3134
- }
3135
- if (direction !== 'vertical') {
3136
- this._content.x += this._velocity.x;
3137
- this._velocity.x *= DECELERATION;
3138
- }
3139
- this.clampScroll();
3140
- if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
3141
- this.stopInertia();
3142
- }
3143
- }
3144
- // ─── Mouse wheel ─────────────────────────────────────
3145
- _onWheel(e) {
3146
- const { direction } = this._scrollConfig;
3147
- e.preventDefault();
3148
- if (direction !== 'horizontal') {
3149
- this._content.y -= e.deltaY;
3150
- }
3151
- if (direction !== 'vertical') {
3152
- this._content.x -= e.deltaX;
3153
- }
3154
- this.clampScroll();
3155
- }
3156
- // ─── Scroll bounds ───────────────────────────────────
3157
- clampScroll() {
3158
- const { direction } = this._scrollConfig;
3159
- const bounds = this._content.getLocalBounds();
3160
- if (direction !== 'horizontal') {
3161
- const contentHeight = bounds.height + bounds.y;
3162
- const maxScroll = Math.min(0, this._viewport.height - contentHeight);
3163
- this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
3164
- }
3165
- if (direction !== 'vertical') {
3166
- const contentWidth = bounds.width + bounds.x;
3167
- const maxScroll = Math.min(0, this._viewport.width - contentWidth);
3168
- this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
3169
- }
3170
- this.updateScrollbar();
3171
- }
3172
- updateScrollbar() {
3173
- if (!this._scrollbar)
3174
- return;
3175
- const { direction } = this._scrollConfig;
3176
- const { width: sbW, padding: sbPad } = this._scrollbarConfig;
3177
- const bounds = this._content.getLocalBounds();
3178
- const isVert = direction !== 'horizontal';
3179
- if (isVert) {
3180
- const contentH = bounds.height + bounds.y;
3181
- if (contentH <= this._viewport.height) {
3182
- this._scrollbar.visible = false;
3183
- return;
3184
- }
3185
- this._scrollbar.visible = true;
3186
- const ratio = this._viewport.height / contentH;
3187
- const thumbH = Math.max(20, this._viewport.height * ratio);
3188
- const scrollRange = this._viewport.height - thumbH;
3189
- const scrollProgress = -this._content.y / (contentH - this._viewport.height);
3190
- this._scrollbar.x = this._viewport.width - sbW - sbPad;
3191
- this._scrollbar.y = scrollProgress * scrollRange;
3192
- this._scrollbar.height = thumbH;
3193
- this._scrollbar.width = sbW;
3194
- }
3195
- else {
3196
- const contentW = bounds.width + bounds.x;
3197
- if (contentW <= this._viewport.width) {
3198
- this._scrollbar.visible = false;
3199
- return;
3200
- }
3201
- this._scrollbar.visible = true;
3202
- const ratio = this._viewport.width / contentW;
3203
- const thumbW = Math.max(20, this._viewport.width * ratio);
3204
- const scrollRange = this._viewport.width - thumbW;
3205
- const scrollProgress = -this._content.x / (contentW - this._viewport.width);
3206
- this._scrollbar.y = this._viewport.height - sbW - sbPad;
3207
- this._scrollbar.x = scrollProgress * scrollRange;
3208
- this._scrollbar.width = thumbW;
3209
- this._scrollbar.height = sbW;
3210
- }
3211
- }
3212
- destroy(options) {
3213
- this.stopInertia();
3214
- this.off('pointerdown', this._onPointerDown, this);
3215
- this.off('pointermove', this._onPointerMove, this);
3216
- this.off('pointerup', this._onPointerUp, this);
3217
- this.off('pointerupoutside', this._onPointerUp, this);
3218
- this._items.length = 0;
3219
- super.destroy(options);
3220
- }
3221
- }
3222
-
3223
- /**
3224
- * Draggable slider with customizable track, fill, and handle views.
3225
- *
3226
- * @example
3227
- * ```ts
3228
- * const volume = new Slider({
3229
- * min: 0, max: 1, value: 0.5,
3230
- * width: 200, height: 8,
3231
- * fillColor: 0xffd700,
3232
- * onUpdate: (v) => console.log('Volume:', v),
3233
- * });
3234
- * ```
3235
- */
3236
- class Slider extends pixi_js.Container {
3237
- __uiComponent = true;
3238
- _track;
3239
- _fill;
3240
- _fillMask;
3241
- _handle;
3242
- _config;
3243
- _value;
3244
- _dragging = false;
3245
- onUpdate = null;
3246
- onChange = null;
3247
- constructor(config = {}) {
3248
- super();
3249
- this._config = {
3250
- min: config.min ?? 0,
3251
- max: config.max ?? 1,
3252
- step: config.step ?? 0,
3253
- width: config.width ?? 200,
3254
- height: config.height ?? 8,
3255
- borderRadius: config.borderRadius ?? 4,
3256
- trackColor: config.trackColor ?? 0x333333,
3257
- fillColor: config.fillColor ?? 0xffd700,
3258
- handleRadius: config.handleRadius ?? 12,
3259
- handleColor: config.handleColor ?? 0xffffff,
3260
- };
3261
- this._value = config.value ?? this._config.min;
3262
- this.onUpdate = config.onUpdate ?? null;
3263
- this.onChange = config.onChange ?? null;
3264
- const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
3265
- // Track
3266
- const customTrack = resolveView(config.trackView);
3267
- if (customTrack) {
3268
- customTrack.width = width;
3269
- customTrack.height = height;
3270
- this._track = customTrack;
3271
- }
3272
- else {
3273
- const g = new pixi_js.Graphics();
3274
- g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
3275
- this._track = g;
3276
- }
3277
- this.addChild(this._track);
3278
- // Fill
3279
- const customFill = resolveView(config.fillView);
3280
- if (customFill) {
3281
- customFill.width = width;
3282
- customFill.height = height;
3283
- this._fill = customFill;
3284
- }
3285
- else {
3286
- const g = new pixi_js.Graphics();
3287
- g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
3288
- this._fill = g;
3289
- }
3290
- this.addChild(this._fill);
3291
- // Fill mask
3292
- this._fillMask = new pixi_js.Graphics();
3293
- this.addChild(this._fillMask);
3294
- this._fill.mask = this._fillMask;
3295
- // Handle
3296
- const customHandle = resolveView(config.handleView);
3297
- if (customHandle) {
3298
- this._handle = customHandle;
3299
- }
3300
- else {
3301
- const g = new pixi_js.Graphics();
3302
- g.circle(0, 0, handleRadius).fill(handleColor);
3303
- this._handle = g;
3304
- }
3305
- this._handle.y = height / 2;
3306
- this.addChild(this._handle);
3307
- // Interaction
3308
- this.eventMode = 'static';
3309
- this.cursor = 'pointer';
3310
- // Hit area covers track + handle overflow
3311
- const hitPad = Math.max(handleRadius - height / 2, 0);
3312
- this.hitArea = { contains: (x, y) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
3313
- this.on('pointerdown', this._onPointerDown, this);
3314
- this.on('globalpointermove', this._onPointerMove, this);
3315
- this.on('pointerup', this._onPointerUp, this);
3316
- this.on('pointerupoutside', this._onPointerUp, this);
3317
- this._updateVisuals();
3318
- }
3319
- /** Current value */
3320
- get value() {
3321
- return this._value;
3322
- }
3323
- set value(v) {
3324
- const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
3325
- if (clamped === this._value)
3326
- return;
3327
- this._value = clamped;
3328
- this._updateVisuals();
3329
- }
3330
- get min() { return this._config.min; }
3331
- get max() { return this._config.max; }
3332
- /** React reconciler update hook */
3333
- updateConfig(changed) {
3334
- if ('value' in changed)
3335
- this.value = changed.value;
3336
- if ('min' in changed) {
3337
- this._config.min = changed.min;
3338
- this._updateVisuals();
3339
- }
3340
- if ('max' in changed) {
3341
- this._config.max = changed.max;
3342
- this._updateVisuals();
3343
- }
3344
- if ('step' in changed)
3345
- this._config.step = changed.step;
3346
- if ('onUpdate' in changed)
3347
- this.onUpdate = changed.onUpdate;
3348
- if ('onChange' in changed)
3349
- this.onChange = changed.onChange;
3350
- }
3351
- _fraction() {
3352
- const { min, max } = this._config;
3353
- return max === min ? 0 : (this._value - min) / (max - min);
3354
- }
3355
- _applyStep(v) {
3356
- const { step, min } = this._config;
3357
- if (step <= 0)
3358
- return v;
3359
- return min + Math.round((v - min) / step) * step;
3360
- }
3361
- _updateVisuals() {
3362
- const frac = this._fraction();
3363
- const w = this._config.width;
3364
- const h = this._config.height;
3365
- // Update fill mask
3366
- this._fillMask.clear();
3367
- this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
3368
- // Update handle position
3369
- this._handle.x = w * frac;
3370
- }
3371
- _valueFromPointer(e) {
3372
- const local = this.toLocal(e.global);
3373
- const frac = Math.max(0, Math.min(1, local.x / this._config.width));
3374
- const { min, max } = this._config;
3375
- return this._applyStep(min + frac * (max - min));
3376
- }
3377
- _onPointerDown(e) {
3378
- this._dragging = true;
3379
- const newValue = this._valueFromPointer(e);
3380
- if (newValue !== this._value) {
3381
- this._value = newValue;
3382
- this._updateVisuals();
3383
- this.onUpdate?.(this._value);
3384
- }
3385
- }
3386
- _onPointerMove(e) {
3387
- if (!this._dragging)
3388
- return;
3389
- const newValue = this._valueFromPointer(e);
3390
- if (newValue !== this._value) {
3391
- this._value = newValue;
3392
- this._updateVisuals();
3393
- this.onUpdate?.(this._value);
3394
- }
3395
- }
3396
- _onPointerUp(_e) {
3397
- if (!this._dragging)
3398
- return;
3399
- this._dragging = false;
3400
- this.onChange?.(this._value);
3401
- }
3402
- destroy(options) {
3403
- this.off('pointerdown', this._onPointerDown, this);
3404
- this.off('globalpointermove', this._onPointerMove, this);
3405
- this.off('pointerup', this._onPointerUp, this);
3406
- this.off('pointerupoutside', this._onPointerUp, this);
3407
- this.onUpdate = null;
3408
- this.onChange = null;
3409
- super.destroy(options);
3410
- }
3411
- }
3412
-
3413
- /**
3414
- * Toggle switch with two states.
3415
- *
3416
- * Supports custom ON/OFF views or auto-generated Graphics-based toggle.
3417
- * Click to toggle, or use `forceSwitch(value)` programmatically.
3418
- *
3419
- * @example
3420
- * ```ts
3421
- * const mute = new Toggle({
3422
- * value: false,
3423
- * onColor: 0x22cc22,
3424
- * onChange: (on) => audioManager.mute(!on),
3425
- * });
3426
- * ```
3427
- */
3428
- class Toggle extends pixi_js.Container {
3429
- __uiComponent = true;
3430
- _value;
3431
- _onView = null;
3432
- _offView = null;
3433
- _handle = null;
3434
- _trackGfx = null;
3435
- _config;
3436
- _useCustomViews;
3437
- onChange = null;
3438
- constructor(config = {}) {
3439
- super();
3440
- this._config = {
3441
- width: config.width ?? 52,
3442
- height: config.height ?? 28,
3443
- onColor: config.onColor ?? 0x22cc22,
3444
- offColor: config.offColor ?? 0x666666,
3445
- handleColor: config.handleColor ?? 0xffffff,
3446
- handleRadius: config.handleRadius ?? 0, // 0 = auto
3447
- animationDuration: config.animationDuration ?? 200,
3448
- };
3449
- this._value = config.value ?? false;
3450
- this.onChange = config.onChange ?? null;
3451
- const customOn = resolveView(config.onView);
3452
- const customOff = resolveView(config.offView);
3453
- this._useCustomViews = !!(customOn || customOff);
3454
- if (this._useCustomViews) {
3455
- // Custom view mode: show/hide ON and OFF views
3456
- if (customOn) {
3457
- this._onView = customOn;
3458
- this._onView.visible = this._value;
3459
- this.addChild(this._onView);
3460
- }
3461
- if (customOff) {
3462
- this._offView = customOff;
3463
- this._offView.visible = !this._value;
3464
- this.addChild(this._offView);
3465
- }
3466
- }
3467
- else {
3468
- // Graphics mode: track + sliding handle
3469
- const { width, height, handleColor } = this._config;
3470
- const handleRadius = this._config.handleRadius || (height / 2 - 3);
3471
- this._config.handleRadius = handleRadius;
3472
- this._trackGfx = new pixi_js.Graphics();
3473
- this.addChild(this._trackGfx);
3474
- this._drawTrack();
3475
- const handle = new pixi_js.Graphics();
3476
- handle.circle(0, 0, handleRadius).fill(handleColor);
3477
- handle.y = height / 2;
3478
- handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
3479
- this._handle = handle;
3480
- this.addChild(handle);
3481
- }
3482
- // Interaction
3483
- this.eventMode = 'static';
3484
- this.cursor = 'pointer';
3485
- this.on('pointertap', this._onTap, this);
3486
- }
3487
- /** Current toggle state */
3488
- get value() {
3489
- return this._value;
3490
- }
3491
- set value(v) {
3492
- if (v === this._value)
3493
- return;
3494
- this.forceSwitch(v);
3495
- }
3496
- /** Programmatically switch to a specific state with animation */
3497
- forceSwitch(value) {
3498
- this._value = value;
3499
- this._animateToState();
3500
- }
3501
- /** React reconciler update hook */
3502
- updateConfig(changed) {
3503
- if ('value' in changed)
3504
- this.value = changed.value;
3505
- if ('onChange' in changed)
3506
- this.onChange = changed.onChange;
3507
- if ('animationDuration' in changed)
3508
- this._config.animationDuration = changed.animationDuration;
3509
- }
3510
- _onTap() {
3511
- this._value = !this._value;
3512
- this._animateToState();
3513
- this.onChange?.(this._value);
3514
- }
3515
- _animateToState() {
3516
- const duration = this._config.animationDuration;
3517
- if (this._useCustomViews) {
3518
- // Custom views: crossfade
3519
- if (this._onView) {
3520
- Tween.killTweensOf(this._onView);
3521
- if (this._value) {
3522
- this._onView.visible = true;
3523
- Tween.to(this._onView, { alpha: 1 }, duration);
3524
- }
3525
- else {
3526
- Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
3527
- if (this._onView)
3528
- this._onView.visible = false;
3529
- });
3530
- }
3531
- }
3532
- if (this._offView) {
3533
- Tween.killTweensOf(this._offView);
3534
- if (!this._value) {
3535
- this._offView.visible = true;
3536
- Tween.to(this._offView, { alpha: 1 }, duration);
3537
- }
3538
- else {
3539
- Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
3540
- if (this._offView)
3541
- this._offView.visible = false;
3542
- });
3543
- }
3544
- }
3545
- }
3546
- else {
3547
- // Graphics mode: slide handle + recolor track
3548
- this._drawTrack();
3549
- if (this._handle) {
3550
- const { width } = this._config;
3551
- const handleRadius = this._config.handleRadius;
3552
- const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
3553
- Tween.killTweensOf(this._handle);
3554
- Tween.to(this._handle, { x: targetX }, duration);
3555
- }
3556
- }
3557
- }
3558
- _drawTrack() {
3559
- if (!this._trackGfx)
3560
- return;
3561
- const { width, height, onColor, offColor } = this._config;
3562
- const radius = height / 2;
3563
- this._trackGfx.clear();
3564
- this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
3565
- }
3566
- destroy(options) {
3567
- this.off('pointertap', this._onTap, this);
3568
- if (this._handle)
3569
- Tween.killTweensOf(this._handle);
3570
- if (this._onView)
3571
- Tween.killTweensOf(this._onView);
3572
- if (this._offView)
3573
- Tween.killTweensOf(this._offView);
3574
- this.onChange = null;
3575
- super.destroy(options);
3576
- }
3577
- }
3578
-
3579
- /**
3580
- * Register all standard PixiJS display objects for JSX use.
3581
- * Call once at app startup before rendering any React scenes.
3582
- */
3583
- function extendPixiElements() {
3584
- extend({
3585
- Container: pixi_js.Container,
3586
- Sprite: pixi_js.Sprite,
3587
- Graphics: pixi_js.Graphics,
3588
- Text: pixi_js.Text,
3589
- AnimatedSprite: pixi_js.AnimatedSprite,
3590
- NineSliceSprite: pixi_js.NineSliceSprite,
3591
- TilingSprite: pixi_js.TilingSprite,
3592
- Mesh: pixi_js.Mesh,
3593
- MeshPlane: pixi_js.MeshPlane,
3594
- MeshRope: pixi_js.MeshRope,
3595
- MeshSimple: pixi_js.MeshSimple,
3596
- BitmapText: pixi_js.BitmapText,
3597
- HTMLText: pixi_js.HTMLText,
3598
- });
3599
- }
3600
- /**
3601
- * Register all engine UI components for JSX use.
3602
- * Call once at app startup before rendering React scenes that use UI components.
3603
- *
3604
- * @example
3605
- * ```ts
3606
- * extendPixiElements();
3607
- * extendUIElements();
3608
- *
3609
- * // Now you can use:
3610
- * // <button text="SPIN" onPress={handler} />
3611
- * // <flexContainer direction="row" gap={16}>...</flexContainer>
3612
- * // <label text="Hello" style-fontSize={24} />
3613
- * ```
3614
- */
3615
- function extendUIElements() {
3616
- extend({
3617
- Button, Label, LabelValue, Panel, FlexContainer, ProgressBar,
3618
- ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
3619
- Slider, Toggle,
3620
- });
3621
- }
3622
- /**
3623
- * Register additional custom components for JSX use.
3624
- * Pass an object mapping component names to their constructors.
3625
- */
3626
- function extendCustomElements(components) {
3627
- extend(components);
3628
- }
3629
-
3630
- /**
3631
- * Base class for all scenes.
3632
- * Provides a root PixiJS Container and lifecycle hooks.
3633
- *
3634
- * @example
3635
- * ```ts
3636
- * class MenuScene extends Scene {
3637
- * async onEnter() {
3638
- * const bg = Sprite.from('menu-bg');
3639
- * this.container.addChild(bg);
3640
- * }
3641
- *
3642
- * onUpdate(dt: number) {
3643
- * // per-frame logic
3644
- * }
3645
- *
3646
- * onResize(width: number, height: number) {
3647
- * // reposition UI
3648
- * }
3649
- * }
3650
- * ```
3651
- */
3652
- class Scene {
3653
- container;
3654
- constructor() {
3655
- this.container = new pixi_js.Container();
3656
- this.container.label = this.constructor.name;
3657
- }
3658
- }
3659
-
3660
- const EngineContext = react.createContext(null);
3661
- function useEngine() {
3662
- const ctx = react.useContext(EngineContext);
3663
- if (!ctx)
3664
- throw new Error('useEngine() must be used inside a ReactScene');
3665
- return ctx;
3666
- }
3667
-
3668
- // ─── Scale Modes ───────────────────────────────────────────
3669
- var ScaleMode;
3670
- (function (ScaleMode) {
3671
- /** Fit inside container, maintain aspect ratio (letterbox/pillarbox) */
3672
- ScaleMode["FIT"] = "FIT";
3673
- /** Fill container, maintain aspect ratio (crop edges) */
3674
- ScaleMode["FILL"] = "FILL";
3675
- /** Stretch to fill (distorts) */
3676
- ScaleMode["STRETCH"] = "STRETCH";
3677
- })(ScaleMode || (ScaleMode = {}));
3678
- // ─── Orientation ───────────────────────────────────────────
3679
- var Orientation;
3680
- (function (Orientation) {
3681
- Orientation["LANDSCAPE"] = "landscape";
3682
- Orientation["PORTRAIT"] = "portrait";
3683
- Orientation["ANY"] = "any";
3684
- })(Orientation || (Orientation = {}));
3685
- // ─── Transition Types ──────────────────────────────────────
3686
- var TransitionType;
3687
- (function (TransitionType) {
3688
- TransitionType["NONE"] = "none";
3689
- TransitionType["FADE"] = "fade";
3690
- TransitionType["SLIDE_LEFT"] = "slide-left";
3691
- TransitionType["SLIDE_RIGHT"] = "slide-right";
3692
- })(TransitionType || (TransitionType = {}));
3693
-
3694
- class ReactScene extends Scene {
3695
- _pixiRoot = null;
3696
- _contextValue = null;
3697
- /** Access the GameApplication instance. */
3698
- getApp() {
3699
- const app = this.__engineApp;
3700
- if (!app) {
3701
- throw new Error('[ReactScene] No GameApplication reference. ' +
3702
- 'Ensure this scene is managed by SceneManager (not instantiated manually).');
3703
- }
3704
- return app;
3705
- }
3706
- async onEnter(data) {
3707
- const app = this.getApp();
3708
- this._contextValue = {
3709
- app,
3710
- sdk: app.sdk,
3711
- audio: app.audio,
3712
- input: app.input,
3713
- viewport: app.viewport,
3714
- gameConfig: app.gameConfig,
3715
- screen: {
3716
- width: app.viewport.width,
3717
- height: app.viewport.height,
3718
- scale: app.viewport.scale,
3719
- },
3720
- isPortrait: app.viewport.orientation === Orientation.PORTRAIT,
3721
- };
3722
- this._pixiRoot = createPixiRoot(this.container);
3723
- this._mountReactTree();
3724
- }
3725
- async onExit() {
3726
- this._pixiRoot?.unmount();
3727
- this._pixiRoot = null;
3728
- this._contextValue = null;
3729
- }
3730
- onResize(width, height) {
3731
- if (!this._contextValue)
3732
- return;
3733
- const app = this.getApp();
3734
- this._contextValue = {
3735
- ...this._contextValue,
3736
- screen: { width, height, scale: app.viewport.scale },
3737
- isPortrait: height > width,
3738
- };
3739
- this._mountReactTree();
3740
- }
3741
- onDestroy() {
3742
- this._pixiRoot?.unmount();
3743
- this._pixiRoot = null;
3744
- this._contextValue = null;
3745
- }
3746
- _mountReactTree() {
3747
- if (!this._pixiRoot || !this._contextValue)
3748
- return;
3749
- this._pixiRoot.render(react.createElement(EngineContext.Provider, { value: this._contextValue }, this.render()));
3750
- }
3751
- }
3752
-
3753
- function useSDK() {
3754
- return useEngine().sdk;
3755
- }
3756
- function useAudio() {
3757
- return useEngine().audio;
3758
- }
3759
- function useInput() {
3760
- return useEngine().input;
3761
- }
3762
- function useViewport() {
3763
- const { screen, isPortrait } = useEngine();
3764
- return { ...screen, isPortrait };
3765
- }
3766
- function useBalance() {
3767
- const { sdk } = useEngine();
3768
- const [balance, setBalance] = react.useState(sdk?.balance ?? 0);
3769
- react.useEffect(() => {
3770
- if (!sdk)
3771
- return;
3772
- const handler = (data) => setBalance(data.balance);
3773
- sdk.on('balanceUpdate', handler);
3774
- return () => {
3775
- sdk.off('balanceUpdate', handler);
3776
- };
3777
- }, [sdk]);
3778
- return balance;
3779
- }
3780
- function useSession() {
3781
- return useEngine().app.session;
3782
- }
3783
- function useGameConfig() {
3784
- return useEngine().gameConfig;
3785
- }
3786
-
3787
- exports.EngineContext = EngineContext;
3788
- exports.ReactScene = ReactScene;
3789
- exports.createPixiRoot = createPixiRoot;
3790
- exports.extend = extend;
3791
- exports.extendCustomElements = extendCustomElements;
3792
- exports.extendPixiElements = extendPixiElements;
3793
- exports.extendUIElements = extendUIElements;
3794
- exports.useAudio = useAudio;
3795
- exports.useBalance = useBalance;
3796
- exports.useEngine = useEngine;
3797
- exports.useGameConfig = useGameConfig;
3798
- exports.useInput = useInput;
3799
- exports.useSDK = useSDK;
3800
- exports.useSession = useSession;
3801
- exports.useViewport = useViewport;
3802
- //# sourceMappingURL=react.cjs.js.map