@energy8platform/game-engine 0.2.1 → 0.4.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 (63) hide show
  1. package/README.md +400 -35
  2. package/dist/animation.cjs.js +191 -1
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.d.ts +117 -1
  5. package/dist/animation.esm.js +192 -3
  6. package/dist/animation.esm.js.map +1 -1
  7. package/dist/audio.cjs.js +66 -16
  8. package/dist/audio.cjs.js.map +1 -1
  9. package/dist/audio.d.ts +4 -0
  10. package/dist/audio.esm.js +66 -16
  11. package/dist/audio.esm.js.map +1 -1
  12. package/dist/core.cjs.js +307 -85
  13. package/dist/core.cjs.js.map +1 -1
  14. package/dist/core.d.ts +60 -1
  15. package/dist/core.esm.js +308 -86
  16. package/dist/core.esm.js.map +1 -1
  17. package/dist/debug.cjs.js +36 -68
  18. package/dist/debug.cjs.js.map +1 -1
  19. package/dist/debug.d.ts +4 -6
  20. package/dist/debug.esm.js +36 -68
  21. package/dist/debug.esm.js.map +1 -1
  22. package/dist/index.cjs.js +997 -475
  23. package/dist/index.cjs.js.map +1 -1
  24. package/dist/index.d.ts +356 -79
  25. package/dist/index.esm.js +983 -478
  26. package/dist/index.esm.js.map +1 -1
  27. package/dist/ui.cjs.js +816 -529
  28. package/dist/ui.cjs.js.map +1 -1
  29. package/dist/ui.d.ts +179 -41
  30. package/dist/ui.esm.js +798 -531
  31. package/dist/ui.esm.js.map +1 -1
  32. package/dist/vite.cjs.js +85 -68
  33. package/dist/vite.cjs.js.map +1 -1
  34. package/dist/vite.d.ts +17 -23
  35. package/dist/vite.esm.js +86 -68
  36. package/dist/vite.esm.js.map +1 -1
  37. package/package.json +19 -5
  38. package/src/animation/SpriteAnimation.ts +210 -0
  39. package/src/animation/Tween.ts +27 -1
  40. package/src/animation/index.ts +2 -0
  41. package/src/audio/AudioManager.ts +64 -15
  42. package/src/core/EventEmitter.ts +7 -1
  43. package/src/core/GameApplication.ts +19 -7
  44. package/src/core/SceneManager.ts +3 -1
  45. package/src/debug/DevBridge.ts +49 -80
  46. package/src/index.ts +22 -0
  47. package/src/input/InputManager.ts +26 -0
  48. package/src/loading/CSSPreloader.ts +7 -33
  49. package/src/loading/LoadingScene.ts +17 -41
  50. package/src/loading/index.ts +1 -0
  51. package/src/loading/logo.ts +95 -0
  52. package/src/types.ts +4 -0
  53. package/src/ui/BalanceDisplay.ts +12 -1
  54. package/src/ui/Button.ts +71 -130
  55. package/src/ui/Layout.ts +286 -0
  56. package/src/ui/Modal.ts +6 -5
  57. package/src/ui/Panel.ts +52 -55
  58. package/src/ui/ProgressBar.ts +52 -57
  59. package/src/ui/ScrollContainer.ts +126 -0
  60. package/src/ui/Toast.ts +19 -13
  61. package/src/ui/index.ts +17 -0
  62. package/src/viewport/ViewportManager.ts +2 -0
  63. package/src/vite/index.ts +103 -83
package/dist/ui.cjs.js CHANGED
@@ -1,411 +1,117 @@
1
1
  'use strict';
2
2
 
3
+ var layout = require('@pixi/layout');
3
4
  var pixi_js = require('pixi.js');
4
-
5
- /**
6
- * Collection of easing functions for use with Tween and Timeline.
7
- *
8
- * All functions take a progress value t (0..1) and return the eased value.
9
- */
10
- const Easing = {
11
- linear: (t) => t,
12
- easeInQuad: (t) => t * t,
13
- easeOutQuad: (t) => t * (2 - t),
14
- easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
15
- easeInCubic: (t) => t * t * t,
16
- easeOutCubic: (t) => --t * t * t + 1,
17
- easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
18
- easeInQuart: (t) => t * t * t * t,
19
- easeOutQuart: (t) => 1 - --t * t * t * t,
20
- easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
21
- easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
22
- easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
23
- easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
24
- easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
25
- easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
26
- easeInOutExpo: (t) => t === 0
27
- ? 0
28
- : t === 1
29
- ? 1
30
- : t < 0.5
31
- ? Math.pow(2, 20 * t - 10) / 2
32
- : (2 - Math.pow(2, -20 * t + 10)) / 2,
33
- easeInBack: (t) => {
34
- const c1 = 1.70158;
35
- const c3 = c1 + 1;
36
- return c3 * t * t * t - c1 * t * t;
37
- },
38
- easeOutBack: (t) => {
39
- const c1 = 1.70158;
40
- const c3 = c1 + 1;
41
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
42
- },
43
- easeInOutBack: (t) => {
44
- const c1 = 1.70158;
45
- const c2 = c1 * 1.525;
46
- return t < 0.5
47
- ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
48
- : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
49
- },
50
- easeOutBounce: (t) => {
51
- const n1 = 7.5625;
52
- const d1 = 2.75;
53
- if (t < 1 / d1)
54
- return n1 * t * t;
55
- if (t < 2 / d1)
56
- return n1 * (t -= 1.5 / d1) * t + 0.75;
57
- if (t < 2.5 / d1)
58
- return n1 * (t -= 2.25 / d1) * t + 0.9375;
59
- return n1 * (t -= 2.625 / d1) * t + 0.984375;
60
- },
61
- easeInBounce: (t) => 1 - Easing.easeOutBounce(1 - t),
62
- easeInOutBounce: (t) => t < 0.5
63
- ? (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
64
- : (1 + Easing.easeOutBounce(2 * t - 1)) / 2,
65
- easeOutElastic: (t) => {
66
- const c4 = (2 * Math.PI) / 3;
67
- return t === 0
68
- ? 0
69
- : t === 1
70
- ? 1
71
- : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
72
- },
73
- easeInElastic: (t) => {
74
- const c4 = (2 * Math.PI) / 3;
75
- return t === 0
76
- ? 0
77
- : t === 1
78
- ? 1
79
- : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
80
- },
81
- };
82
-
83
- /**
84
- * Lightweight tween system integrated with PixiJS Ticker.
85
- * Zero external dependencies — no GSAP required.
86
- *
87
- * All tweens return a Promise that resolves on completion.
88
- *
89
- * @example
90
- * ```ts
91
- * // Fade in a sprite
92
- * await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
93
- *
94
- * // Move and wait
95
- * await Tween.to(sprite, { x: 500 }, 300);
96
- *
97
- * // From a starting value
98
- * await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
99
- * ```
100
- */
101
- class Tween {
102
- static _tweens = [];
103
- static _tickerAdded = false;
104
- /**
105
- * Animate properties from current values to target values.
106
- *
107
- * @param target - Object to animate (Sprite, Container, etc.)
108
- * @param props - Target property values
109
- * @param duration - Duration in milliseconds
110
- * @param easing - Easing function (default: easeOutQuad)
111
- * @param onUpdate - Progress callback (0..1)
112
- */
113
- static to(target, props, duration, easing, onUpdate) {
114
- return new Promise((resolve) => {
115
- // Capture starting values
116
- const from = {};
117
- for (const key of Object.keys(props)) {
118
- from[key] = Tween.getProperty(target, key);
119
- }
120
- const tween = {
121
- target,
122
- from,
123
- to: { ...props },
124
- duration: Math.max(1, duration),
125
- easing: easing ?? Easing.easeOutQuad,
126
- elapsed: 0,
127
- delay: 0,
128
- resolve,
129
- onUpdate,
130
- };
131
- Tween._tweens.push(tween);
132
- Tween.ensureTicker();
133
- });
134
- }
135
- /**
136
- * Animate properties from given values to current values.
137
- */
138
- static from(target, props, duration, easing, onUpdate) {
139
- // Capture current values as "to"
140
- const to = {};
141
- for (const key of Object.keys(props)) {
142
- to[key] = Tween.getProperty(target, key);
143
- Tween.setProperty(target, key, props[key]);
144
- }
145
- return Tween.to(target, to, duration, easing, onUpdate);
146
- }
147
- /**
148
- * Animate from one set of values to another.
149
- */
150
- static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
151
- // Set starting values
152
- for (const key of Object.keys(fromProps)) {
153
- Tween.setProperty(target, key, fromProps[key]);
154
- }
155
- return Tween.to(target, toProps, duration, easing, onUpdate);
156
- }
157
- /**
158
- * Wait for a given duration (useful in timelines).
159
- */
160
- static delay(ms) {
161
- return new Promise((resolve) => setTimeout(resolve, ms));
162
- }
163
- /**
164
- * Kill all tweens on a target.
165
- */
166
- static killTweensOf(target) {
167
- Tween._tweens = Tween._tweens.filter((tw) => {
168
- if (tw.target === target) {
169
- tw.resolve();
170
- return false;
171
- }
172
- return true;
173
- });
174
- }
175
- /**
176
- * Kill all active tweens.
177
- */
178
- static killAll() {
179
- for (const tw of Tween._tweens) {
180
- tw.resolve();
181
- }
182
- Tween._tweens.length = 0;
183
- }
184
- /** Number of active tweens */
185
- static get activeTweens() {
186
- return Tween._tweens.length;
187
- }
188
- // ─── Internal ──────────────────────────────────────────
189
- static ensureTicker() {
190
- if (Tween._tickerAdded)
191
- return;
192
- Tween._tickerAdded = true;
193
- pixi_js.Ticker.shared.add(Tween.tick);
194
- }
195
- static tick = (ticker) => {
196
- const dt = ticker.deltaMS;
197
- const completed = [];
198
- for (const tw of Tween._tweens) {
199
- tw.elapsed += dt;
200
- if (tw.elapsed < tw.delay)
201
- continue;
202
- const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
203
- const t = tw.easing(raw);
204
- // Interpolate each property
205
- for (const key of Object.keys(tw.to)) {
206
- const start = tw.from[key];
207
- const end = tw.to[key];
208
- const value = start + (end - start) * t;
209
- Tween.setProperty(tw.target, key, value);
210
- }
211
- tw.onUpdate?.(raw);
212
- if (raw >= 1) {
213
- completed.push(tw);
214
- }
215
- }
216
- // Remove completed tweens
217
- for (const tw of completed) {
218
- const idx = Tween._tweens.indexOf(tw);
219
- if (idx !== -1)
220
- Tween._tweens.splice(idx, 1);
221
- tw.resolve();
222
- }
223
- // Remove ticker when no active tweens
224
- if (Tween._tweens.length === 0 && Tween._tickerAdded) {
225
- pixi_js.Ticker.shared.remove(Tween.tick);
226
- Tween._tickerAdded = false;
227
- }
228
- };
229
- /**
230
- * Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
231
- */
232
- static getProperty(target, key) {
233
- const parts = key.split('.');
234
- let obj = target;
235
- for (let i = 0; i < parts.length - 1; i++) {
236
- obj = obj[parts[i]];
237
- }
238
- return obj[parts[parts.length - 1]] ?? 0;
239
- }
240
- /**
241
- * Set a potentially nested property.
242
- */
243
- static setProperty(target, key, value) {
244
- const parts = key.split('.');
245
- let obj = target;
246
- for (let i = 0; i < parts.length - 1; i++) {
247
- obj = obj[parts[i]];
248
- }
249
- obj[parts[parts.length - 1]] = value;
250
- }
251
- }
5
+ var ui = require('@pixi/ui');
6
+ var components = require('@pixi/layout/components');
252
7
 
253
8
  const DEFAULT_COLORS = {
254
- normal: 0xffd700,
9
+ default: 0xffd700,
255
10
  hover: 0xffe44d,
256
11
  pressed: 0xccac00,
257
12
  disabled: 0x666666,
258
13
  };
14
+ function makeGraphicsView(w, h, radius, color) {
15
+ const g = new pixi_js.Graphics();
16
+ g.roundRect(0, 0, w, h, radius).fill(color);
17
+ // Highlight overlay
18
+ g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
19
+ return g;
20
+ }
259
21
  /**
260
- * Interactive button component with state management and animation.
22
+ * Interactive button component powered by `@pixi/ui` FancyButton.
261
23
  *
262
- * Supports both texture-based and Graphics-based rendering.
24
+ * Supports both texture-based and Graphics-based rendering with
25
+ * per-state views, press animation, and text.
263
26
  *
264
27
  * @example
265
28
  * ```ts
266
29
  * const btn = new Button({
267
30
  * width: 200, height: 60, borderRadius: 12,
268
- * colors: { normal: 0x22aa22, hover: 0x33cc33 },
31
+ * colors: { default: 0x22aa22, hover: 0x33cc33 },
32
+ * text: 'SPIN',
269
33
  * });
270
34
  *
271
- * btn.onTap = () => console.log('Clicked!');
35
+ * btn.onPress.connect(() => console.log('Clicked!'));
272
36
  * scene.container.addChild(btn);
273
37
  * ```
274
38
  */
275
- class Button extends pixi_js.Container {
276
- _state = 'normal';
277
- _bg;
278
- _sprites = {};
279
- _config;
280
- /** Called when the button is tapped/clicked */
281
- onTap;
282
- /** Called when the button state changes */
283
- onStateChange;
39
+ class Button extends ui.FancyButton {
40
+ _buttonConfig;
284
41
  constructor(config = {}) {
285
- super();
286
- this._config = {
287
- width: 200,
288
- height: 60,
289
- borderRadius: 8,
290
- pressScale: 0.95,
291
- animationDuration: 100,
42
+ const resolvedConfig = {
43
+ width: config.width ?? 200,
44
+ height: config.height ?? 60,
45
+ borderRadius: config.borderRadius ?? 8,
46
+ pressScale: config.pressScale ?? 0.95,
47
+ animationDuration: config.animationDuration ?? 100,
292
48
  ...config,
293
49
  };
294
- // Create Graphics background
295
- this._bg = new pixi_js.Graphics();
296
- this.addChild(this._bg);
297
- // Create texture sprites if provided
50
+ const colorMap = { ...DEFAULT_COLORS, ...config.colors };
51
+ const { width, height, borderRadius } = resolvedConfig;
52
+ // Build FancyButton options
53
+ const options = {
54
+ anchor: 0.5,
55
+ animations: {
56
+ hover: {
57
+ props: { scale: { x: 1.03, y: 1.03 } },
58
+ duration: resolvedConfig.animationDuration,
59
+ },
60
+ pressed: {
61
+ props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
62
+ duration: resolvedConfig.animationDuration,
63
+ },
64
+ },
65
+ };
66
+ // Texture-based views
298
67
  if (config.textures) {
299
- for (const [state, tex] of Object.entries(config.textures)) {
300
- const texture = typeof tex === 'string' ? pixi_js.Texture.from(tex) : tex;
301
- const sprite = new pixi_js.Sprite(texture);
302
- sprite.anchor.set(0.5);
303
- sprite.visible = state === 'normal';
304
- this._sprites[state] = sprite;
305
- this.addChild(sprite);
306
- }
68
+ if (config.textures.default)
69
+ options.defaultView = config.textures.default;
70
+ if (config.textures.hover)
71
+ options.hoverView = config.textures.hover;
72
+ if (config.textures.pressed)
73
+ options.pressedView = config.textures.pressed;
74
+ if (config.textures.disabled)
75
+ options.disabledView = config.textures.disabled;
76
+ }
77
+ else {
78
+ // Graphics-based views
79
+ options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
80
+ options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
81
+ options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
82
+ options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
83
+ }
84
+ // Text
85
+ if (config.text) {
86
+ options.text = config.text;
307
87
  }
308
- // Make interactive
309
- this.eventMode = 'static';
310
- this.cursor = 'pointer';
311
- // Set up hit area for Graphics-based
312
- this.pivot.set(this._config.width / 2, this._config.height / 2);
313
- // Bind events
314
- this.on('pointerover', this.onPointerOver);
315
- this.on('pointerout', this.onPointerOut);
316
- this.on('pointerdown', this.onPointerDown);
317
- this.on('pointerup', this.onPointerUp);
318
- this.on('pointertap', this.onPointerTap);
319
- // Initial render
320
- this.setState('normal');
88
+ super(options);
89
+ this._buttonConfig = resolvedConfig;
321
90
  if (config.disabled) {
322
- this.disable();
91
+ this.enabled = false;
323
92
  }
324
93
  }
325
- /** Current button state */
326
- get state() {
327
- return this._state;
328
- }
329
94
  /** Enable the button */
330
95
  enable() {
331
- if (this._state === 'disabled') {
332
- this.setState('normal');
333
- this.eventMode = 'static';
334
- this.cursor = 'pointer';
335
- }
96
+ this.enabled = true;
336
97
  }
337
98
  /** Disable the button */
338
99
  disable() {
339
- this.setState('disabled');
340
- this.eventMode = 'none';
341
- this.cursor = 'default';
100
+ this.enabled = false;
342
101
  }
343
102
  /** Whether the button is disabled */
344
103
  get disabled() {
345
- return this._state === 'disabled';
104
+ return !this.enabled;
346
105
  }
347
- setState(state) {
348
- if (this._state === state)
349
- return;
350
- this._state = state;
351
- this.render();
352
- this.onStateChange?.(state);
353
- }
354
- render() {
355
- const { width, height, borderRadius, colors } = this._config;
356
- const colorMap = { ...DEFAULT_COLORS, ...colors };
357
- // Update Graphics
358
- this._bg.clear();
359
- this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
360
- // Add highlight for normal/hover
361
- if (this._state === 'normal' || this._state === 'hover') {
362
- this._bg
363
- .roundRect(2, 2, width - 4, height * 0.45, borderRadius)
364
- .fill({ color: 0xffffff, alpha: 0.1 });
365
- }
366
- // Update sprite visibility
367
- for (const [state, sprite] of Object.entries(this._sprites)) {
368
- if (sprite)
369
- sprite.visible = state === this._state;
370
- }
371
- // Fall back to normal sprite if state sprite doesn't exist
372
- if (!this._sprites[this._state] && this._sprites.normal) {
373
- this._sprites.normal.visible = true;
374
- }
375
- }
376
- onPointerOver = () => {
377
- if (this._state === 'disabled')
378
- return;
379
- this.setState('hover');
380
- };
381
- onPointerOut = () => {
382
- if (this._state === 'disabled')
383
- return;
384
- this.setState('normal');
385
- Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
386
- };
387
- onPointerDown = () => {
388
- if (this._state === 'disabled')
389
- return;
390
- this.setState('pressed');
391
- const s = this._config.pressScale;
392
- Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
393
- };
394
- onPointerUp = () => {
395
- if (this._state === 'disabled')
396
- return;
397
- this.setState('hover');
398
- Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
399
- };
400
- onPointerTap = () => {
401
- if (this._state === 'disabled')
402
- return;
403
- this.onTap?.();
404
- };
405
106
  }
406
107
 
108
+ function makeBarGraphics(w, h, radius, color) {
109
+ return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
110
+ }
407
111
  /**
408
- * Horizontal progress bar with optional smooth fill animation.
112
+ * Horizontal progress bar powered by `@pixi/ui` ProgressBar.
113
+ *
114
+ * Provides optional smooth animated fill via per-frame `update()`.
409
115
  *
410
116
  * @example
411
117
  * ```ts
@@ -415,33 +121,48 @@ class Button extends pixi_js.Container {
415
121
  * ```
416
122
  */
417
123
  class ProgressBar extends pixi_js.Container {
418
- _track;
419
- _fill;
420
- _border;
124
+ _bar;
125
+ _borderGfx;
421
126
  _config;
422
127
  _progress = 0;
423
128
  _displayedProgress = 0;
424
129
  constructor(config = {}) {
425
130
  super();
426
131
  this._config = {
427
- width: 300,
428
- height: 16,
429
- borderRadius: 8,
430
- fillColor: 0xffd700,
431
- trackColor: 0x333333,
432
- borderColor: 0x555555,
433
- borderWidth: 1,
434
- animated: true,
435
- animationSpeed: 0.1,
436
- ...config,
132
+ width: config.width ?? 300,
133
+ height: config.height ?? 16,
134
+ borderRadius: config.borderRadius ?? 8,
135
+ fillColor: config.fillColor ?? 0xffd700,
136
+ trackColor: config.trackColor ?? 0x333333,
137
+ borderColor: config.borderColor ?? 0x555555,
138
+ borderWidth: config.borderWidth ?? 1,
139
+ animated: config.animated ?? true,
140
+ animationSpeed: config.animationSpeed ?? 0.1,
141
+ };
142
+ const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
143
+ const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
144
+ const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
145
+ const options = {
146
+ bg: bgGraphics,
147
+ fill: fillGraphics,
148
+ fillPaddings: {
149
+ top: borderWidth,
150
+ right: borderWidth,
151
+ bottom: borderWidth,
152
+ left: borderWidth,
153
+ },
154
+ progress: 0,
437
155
  };
438
- this._track = new pixi_js.Graphics();
439
- this._fill = new pixi_js.Graphics();
440
- this._border = new pixi_js.Graphics();
441
- this.addChild(this._track, this._fill, this._border);
442
- this.drawTrack();
443
- this.drawBorder();
444
- this.drawFill(0);
156
+ this._bar = new ui.ProgressBar(options);
157
+ this.addChild(this._bar);
158
+ // Border overlay
159
+ this._borderGfx = new pixi_js.Graphics();
160
+ if (borderColor !== undefined && borderWidth > 0) {
161
+ this._borderGfx
162
+ .roundRect(0, 0, width, height, borderRadius)
163
+ .stroke({ color: borderColor, width: borderWidth });
164
+ }
165
+ this.addChild(this._borderGfx);
445
166
  }
446
167
  /** Get/set progress (0..1) */
447
168
  get progress() {
@@ -451,13 +172,13 @@ class ProgressBar extends pixi_js.Container {
451
172
  this._progress = Math.max(0, Math.min(1, value));
452
173
  if (!this._config.animated) {
453
174
  this._displayedProgress = this._progress;
454
- this.drawFill(this._displayedProgress);
175
+ this._bar.progress = this._displayedProgress * 100;
455
176
  }
456
177
  }
457
178
  /**
458
179
  * Call each frame if animated is true.
459
180
  */
460
- update(dt) {
181
+ update(_dt) {
461
182
  if (!this._config.animated)
462
183
  return;
463
184
  if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
@@ -466,35 +187,7 @@ class ProgressBar extends pixi_js.Container {
466
187
  }
467
188
  this._displayedProgress +=
468
189
  (this._progress - this._displayedProgress) * this._config.animationSpeed;
469
- this.drawFill(this._displayedProgress);
470
- }
471
- drawTrack() {
472
- const { width, height, borderRadius, trackColor } = this._config;
473
- this._track.clear();
474
- this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
475
- }
476
- drawBorder() {
477
- const { width, height, borderRadius, borderColor, borderWidth } = this._config;
478
- this._border.clear();
479
- this._border
480
- .roundRect(0, 0, width, height, borderRadius)
481
- .stroke({ color: borderColor, width: borderWidth });
482
- }
483
- drawFill(progress) {
484
- const { width, height, borderRadius, fillColor, borderWidth } = this._config;
485
- const innerWidth = width - borderWidth * 2;
486
- const innerHeight = height - borderWidth * 2;
487
- const fillWidth = Math.max(0, innerWidth * progress);
488
- this._fill.clear();
489
- if (fillWidth > 0) {
490
- this._fill.x = borderWidth;
491
- this._fill.y = borderWidth;
492
- this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
493
- // Highlight
494
- this._fill
495
- .roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
496
- .fill({ color: 0xffffff, alpha: 0.15 });
497
- }
190
+ this._bar.progress = this._displayedProgress * 100;
498
191
  }
499
192
  }
500
193
 
@@ -590,7 +283,10 @@ class Label extends pixi_js.Container {
590
283
  }
591
284
 
592
285
  /**
593
- * Background panel that can use either Graphics or 9-slice sprite.
286
+ * Background panel powered by `@pixi/layout` LayoutContainer.
287
+ *
288
+ * Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
289
+ * Children added to `content` participate in flexbox layout automatically.
594
290
  *
595
291
  * @example
596
292
  * ```ts
@@ -605,75 +301,148 @@ class Label extends pixi_js.Container {
605
301
  * });
606
302
  * ```
607
303
  */
608
- class Panel extends pixi_js.Container {
609
- _bg;
610
- _content;
611
- _config;
304
+ class Panel extends components.LayoutContainer {
305
+ _panelConfig;
612
306
  constructor(config = {}) {
613
- super();
614
- this._config = {
615
- width: 400,
616
- height: 300,
617
- padding: 16,
618
- backgroundAlpha: 1,
307
+ const resolvedConfig = {
308
+ width: config.width ?? 400,
309
+ height: config.height ?? 300,
310
+ padding: config.padding ?? 16,
311
+ backgroundAlpha: config.backgroundAlpha ?? 1,
619
312
  ...config,
620
313
  };
621
- // Create background
314
+ // If using a 9-slice texture, pass it as a custom background
315
+ let customBackground;
622
316
  if (config.nineSliceTexture) {
623
317
  const texture = typeof config.nineSliceTexture === 'string'
624
318
  ? pixi_js.Texture.from(config.nineSliceTexture)
625
319
  : config.nineSliceTexture;
626
320
  const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
627
- this._bg = new pixi_js.NineSliceSprite({
321
+ const nineSlice = new pixi_js.NineSliceSprite({
628
322
  texture,
629
323
  leftWidth: left,
630
324
  topHeight: top,
631
325
  rightWidth: right,
632
326
  bottomHeight: bottom,
633
327
  });
634
- this._bg.width = this._config.width;
635
- this._bg.height = this._config.height;
328
+ nineSlice.width = resolvedConfig.width;
329
+ nineSlice.height = resolvedConfig.height;
330
+ nineSlice.alpha = resolvedConfig.backgroundAlpha;
331
+ customBackground = nineSlice;
636
332
  }
637
- else {
638
- this._bg = new pixi_js.Graphics();
639
- this.drawGraphicsBg();
333
+ super(customBackground ? { background: customBackground } : undefined);
334
+ this._panelConfig = resolvedConfig;
335
+ // Apply layout styles
336
+ const layoutStyles = {
337
+ width: resolvedConfig.width,
338
+ height: resolvedConfig.height,
339
+ padding: resolvedConfig.padding,
340
+ flexDirection: 'column',
341
+ };
342
+ // Graphics-based background via layout styles
343
+ if (!config.nineSliceTexture) {
344
+ layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
345
+ layoutStyles.borderRadius = config.borderRadius ?? 0;
346
+ if (config.borderColor !== undefined && config.borderWidth) {
347
+ layoutStyles.borderColor = config.borderColor;
348
+ layoutStyles.borderWidth = config.borderWidth;
349
+ }
350
+ }
351
+ this.layout = layoutStyles;
352
+ if (!config.nineSliceTexture) {
353
+ this.background.alpha = resolvedConfig.backgroundAlpha;
640
354
  }
641
- this._bg.alpha = this._config.backgroundAlpha;
642
- this.addChild(this._bg);
643
- // Content container with padding
644
- this._content = new pixi_js.Container();
645
- this._content.x = this._config.padding;
646
- this._content.y = this._config.padding;
647
- this.addChild(this._content);
648
355
  }
649
- /** Content container add children here */
356
+ /** Access the content container (children added here participate in layout) */
650
357
  get content() {
651
- return this._content;
358
+ return this.overflowContainer;
652
359
  }
653
360
  /** Resize the panel */
654
361
  setSize(width, height) {
655
- this._config.width = width;
656
- this._config.height = height;
657
- if (this._bg instanceof pixi_js.Graphics) {
658
- this.drawGraphicsBg();
659
- }
660
- else {
661
- this._bg.width = width;
662
- this._bg.height = height;
663
- }
664
- }
665
- drawGraphicsBg() {
666
- const bg = this._bg;
667
- const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
668
- bg.clear();
669
- bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
670
- if (borderColor !== undefined && borderWidth) {
671
- bg.roundRect(0, 0, width, height, borderRadius ?? 0)
672
- .stroke({ color: borderColor, width: borderWidth });
673
- }
362
+ this._panelConfig.width = width;
363
+ this._panelConfig.height = height;
364
+ this._layout?.setStyle({ width, height });
674
365
  }
675
366
  }
676
367
 
368
+ /**
369
+ * Collection of easing functions for use with Tween and Timeline.
370
+ *
371
+ * All functions take a progress value t (0..1) and return the eased value.
372
+ */
373
+ const Easing = {
374
+ linear: (t) => t,
375
+ easeInQuad: (t) => t * t,
376
+ easeOutQuad: (t) => t * (2 - t),
377
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
378
+ easeInCubic: (t) => t * t * t,
379
+ easeOutCubic: (t) => --t * t * t + 1,
380
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
381
+ easeInQuart: (t) => t * t * t * t,
382
+ easeOutQuart: (t) => 1 - --t * t * t * t,
383
+ easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
384
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
385
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
386
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
387
+ easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
388
+ easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
389
+ easeInOutExpo: (t) => t === 0
390
+ ? 0
391
+ : t === 1
392
+ ? 1
393
+ : t < 0.5
394
+ ? Math.pow(2, 20 * t - 10) / 2
395
+ : (2 - Math.pow(2, -20 * t + 10)) / 2,
396
+ easeInBack: (t) => {
397
+ const c1 = 1.70158;
398
+ const c3 = c1 + 1;
399
+ return c3 * t * t * t - c1 * t * t;
400
+ },
401
+ easeOutBack: (t) => {
402
+ const c1 = 1.70158;
403
+ const c3 = c1 + 1;
404
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
405
+ },
406
+ easeInOutBack: (t) => {
407
+ const c1 = 1.70158;
408
+ const c2 = c1 * 1.525;
409
+ return t < 0.5
410
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
411
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
412
+ },
413
+ easeOutBounce: (t) => {
414
+ const n1 = 7.5625;
415
+ const d1 = 2.75;
416
+ if (t < 1 / d1)
417
+ return n1 * t * t;
418
+ if (t < 2 / d1)
419
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
420
+ if (t < 2.5 / d1)
421
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
422
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
423
+ },
424
+ easeInBounce: (t) => 1 - Easing.easeOutBounce(1 - t),
425
+ easeInOutBounce: (t) => t < 0.5
426
+ ? (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
427
+ : (1 + Easing.easeOutBounce(2 * t - 1)) / 2,
428
+ easeOutElastic: (t) => {
429
+ const c4 = (2 * Math.PI) / 3;
430
+ return t === 0
431
+ ? 0
432
+ : t === 1
433
+ ? 1
434
+ : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
435
+ },
436
+ easeInElastic: (t) => {
437
+ const c4 = (2 * Math.PI) / 3;
438
+ return t === 0
439
+ ? 0
440
+ : t === 1
441
+ ? 1
442
+ : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
443
+ },
444
+ };
445
+
677
446
  /**
678
447
  * Reactive balance display component.
679
448
  *
@@ -696,6 +465,7 @@ class BalanceDisplay extends pixi_js.Container {
696
465
  _currentValue = 0;
697
466
  _displayedValue = 0;
698
467
  _animating = false;
468
+ _animationCancelled = false;
699
469
  constructor(config = {}) {
700
470
  super();
701
471
  this._config = {
@@ -757,11 +527,20 @@ class BalanceDisplay extends pixi_js.Container {
757
527
  this.updateDisplay();
758
528
  }
759
529
  async animateValue(from, to) {
530
+ if (this._animating) {
531
+ this._animationCancelled = true;
532
+ }
760
533
  this._animating = true;
534
+ this._animationCancelled = false;
761
535
  const duration = this._config.animationDuration;
762
536
  const startTime = Date.now();
763
537
  return new Promise((resolve) => {
764
538
  const tick = () => {
539
+ if (this._animationCancelled) {
540
+ this._animating = false;
541
+ resolve();
542
+ return;
543
+ }
765
544
  const elapsed = Date.now() - startTime;
766
545
  const t = Math.min(elapsed / duration, 1);
767
546
  const eased = Easing.easeOutCubic(t);
@@ -827,70 +606,265 @@ class WinDisplay extends pixi_js.Container {
827
606
  ...config.style,
828
607
  },
829
608
  });
830
- this.addChild(this._label);
831
- this.visible = false;
609
+ this.addChild(this._label);
610
+ this.visible = false;
611
+ }
612
+ /**
613
+ * Show a win with countup animation.
614
+ *
615
+ * @param amount - Win amount
616
+ * @returns Promise that resolves when the animation completes
617
+ */
618
+ async showWin(amount) {
619
+ this.visible = true;
620
+ this._cancelCountup = false;
621
+ this.alpha = 1;
622
+ const duration = this._config.countupDuration;
623
+ const startTime = Date.now();
624
+ // Scale pop
625
+ this.scale.set(0.5);
626
+ return new Promise((resolve) => {
627
+ const tick = () => {
628
+ if (this._cancelCountup) {
629
+ this.displayAmount(amount);
630
+ resolve();
631
+ return;
632
+ }
633
+ const elapsed = Date.now() - startTime;
634
+ const t = Math.min(elapsed / duration, 1);
635
+ const eased = Easing.easeOutCubic(t);
636
+ // Countup
637
+ const current = amount * eased;
638
+ this.displayAmount(current);
639
+ // Scale animation
640
+ const scaleT = Math.min(elapsed / 300, 1);
641
+ const scaleEased = Easing.easeOutBack(scaleT);
642
+ const targetScale = 1;
643
+ this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
644
+ if (t < 1) {
645
+ requestAnimationFrame(tick);
646
+ }
647
+ else {
648
+ this.displayAmount(amount);
649
+ this.scale.set(1);
650
+ resolve();
651
+ }
652
+ };
653
+ requestAnimationFrame(tick);
654
+ });
655
+ }
656
+ /**
657
+ * Skip the countup animation and show the final amount immediately.
658
+ */
659
+ skipCountup(amount) {
660
+ this._cancelCountup = true;
661
+ this.displayAmount(amount);
662
+ this.scale.set(1);
663
+ }
664
+ /**
665
+ * Hide the win display.
666
+ */
667
+ hide() {
668
+ this.visible = false;
669
+ this._label.text = '';
670
+ }
671
+ displayAmount(amount) {
672
+ this._label.setCurrency(amount, this._config.currency, this._config.locale);
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Lightweight tween system integrated with PixiJS Ticker.
678
+ * Zero external dependencies — no GSAP required.
679
+ *
680
+ * All tweens return a Promise that resolves on completion.
681
+ *
682
+ * @example
683
+ * ```ts
684
+ * // Fade in a sprite
685
+ * await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
686
+ *
687
+ * // Move and wait
688
+ * await Tween.to(sprite, { x: 500 }, 300);
689
+ *
690
+ * // From a starting value
691
+ * await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
692
+ * ```
693
+ */
694
+ class Tween {
695
+ static _tweens = [];
696
+ static _tickerAdded = false;
697
+ /**
698
+ * Animate properties from current values to target values.
699
+ *
700
+ * @param target - Object to animate (Sprite, Container, etc.)
701
+ * @param props - Target property values
702
+ * @param duration - Duration in milliseconds
703
+ * @param easing - Easing function (default: easeOutQuad)
704
+ * @param onUpdate - Progress callback (0..1)
705
+ */
706
+ static to(target, props, duration, easing, onUpdate) {
707
+ return new Promise((resolve) => {
708
+ // Capture starting values
709
+ const from = {};
710
+ for (const key of Object.keys(props)) {
711
+ from[key] = Tween.getProperty(target, key);
712
+ }
713
+ const tween = {
714
+ target,
715
+ from,
716
+ to: { ...props },
717
+ duration: Math.max(1, duration),
718
+ easing: easing ?? Easing.easeOutQuad,
719
+ elapsed: 0,
720
+ delay: 0,
721
+ resolve,
722
+ onUpdate,
723
+ };
724
+ Tween._tweens.push(tween);
725
+ Tween.ensureTicker();
726
+ });
832
727
  }
833
728
  /**
834
- * Show a win with countup animation.
835
- *
836
- * @param amount - Win amount
837
- * @returns Promise that resolves when the animation completes
729
+ * Animate properties from given values to current values.
838
730
  */
839
- async showWin(amount) {
840
- this.visible = true;
841
- this._cancelCountup = false;
842
- this.alpha = 1;
843
- const duration = this._config.countupDuration;
844
- const startTime = Date.now();
845
- // Scale pop
846
- this.scale.set(0.5);
731
+ static from(target, props, duration, easing, onUpdate) {
732
+ // Capture current values as "to"
733
+ const to = {};
734
+ for (const key of Object.keys(props)) {
735
+ to[key] = Tween.getProperty(target, key);
736
+ Tween.setProperty(target, key, props[key]);
737
+ }
738
+ return Tween.to(target, to, duration, easing, onUpdate);
739
+ }
740
+ /**
741
+ * Animate from one set of values to another.
742
+ */
743
+ static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
744
+ // Set starting values
745
+ for (const key of Object.keys(fromProps)) {
746
+ Tween.setProperty(target, key, fromProps[key]);
747
+ }
748
+ return Tween.to(target, toProps, duration, easing, onUpdate);
749
+ }
750
+ /**
751
+ * Wait for a given duration (useful in timelines).
752
+ * Uses PixiJS Ticker for consistent timing with other tweens.
753
+ */
754
+ static delay(ms) {
847
755
  return new Promise((resolve) => {
848
- const tick = () => {
849
- if (this._cancelCountup) {
850
- this.displayAmount(amount);
851
- resolve();
852
- return;
853
- }
854
- const elapsed = Date.now() - startTime;
855
- const t = Math.min(elapsed / duration, 1);
856
- const eased = Easing.easeOutCubic(t);
857
- // Countup
858
- const current = amount * eased;
859
- this.displayAmount(current);
860
- // Scale animation
861
- const scaleT = Math.min(elapsed / 300, 1);
862
- const scaleEased = Easing.easeOutBack(scaleT);
863
- const targetScale = 1;
864
- this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
865
- if (t < 1) {
866
- requestAnimationFrame(tick);
867
- }
868
- else {
869
- this.displayAmount(amount);
870
- this.scale.set(1);
756
+ let elapsed = 0;
757
+ const onTick = (ticker) => {
758
+ elapsed += ticker.deltaMS;
759
+ if (elapsed >= ms) {
760
+ pixi_js.Ticker.shared.remove(onTick);
871
761
  resolve();
872
762
  }
873
763
  };
874
- requestAnimationFrame(tick);
764
+ pixi_js.Ticker.shared.add(onTick);
875
765
  });
876
766
  }
877
767
  /**
878
- * Skip the countup animation and show the final amount immediately.
768
+ * Kill all tweens on a target.
879
769
  */
880
- skipCountup(amount) {
881
- this._cancelCountup = true;
882
- this.displayAmount(amount);
883
- this.scale.set(1);
770
+ static killTweensOf(target) {
771
+ Tween._tweens = Tween._tweens.filter((tw) => {
772
+ if (tw.target === target) {
773
+ tw.resolve();
774
+ return false;
775
+ }
776
+ return true;
777
+ });
884
778
  }
885
779
  /**
886
- * Hide the win display.
780
+ * Kill all active tweens.
887
781
  */
888
- hide() {
889
- this.visible = false;
890
- this._label.text = '';
782
+ static killAll() {
783
+ for (const tw of Tween._tweens) {
784
+ tw.resolve();
785
+ }
786
+ Tween._tweens.length = 0;
891
787
  }
892
- displayAmount(amount) {
893
- this._label.setCurrency(amount, this._config.currency, this._config.locale);
788
+ /** Number of active tweens */
789
+ static get activeTweens() {
790
+ return Tween._tweens.length;
791
+ }
792
+ /**
793
+ * Reset the tween system — kill all tweens and remove the ticker.
794
+ * Useful for cleanup between game instances, tests, or hot-reload.
795
+ */
796
+ static reset() {
797
+ for (const tw of Tween._tweens) {
798
+ tw.resolve();
799
+ }
800
+ Tween._tweens.length = 0;
801
+ if (Tween._tickerAdded) {
802
+ pixi_js.Ticker.shared.remove(Tween.tick);
803
+ Tween._tickerAdded = false;
804
+ }
805
+ }
806
+ // ─── Internal ──────────────────────────────────────────
807
+ static ensureTicker() {
808
+ if (Tween._tickerAdded)
809
+ return;
810
+ Tween._tickerAdded = true;
811
+ pixi_js.Ticker.shared.add(Tween.tick);
812
+ }
813
+ static tick = (ticker) => {
814
+ const dt = ticker.deltaMS;
815
+ const completed = [];
816
+ for (const tw of Tween._tweens) {
817
+ tw.elapsed += dt;
818
+ if (tw.elapsed < tw.delay)
819
+ continue;
820
+ const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
821
+ const t = tw.easing(raw);
822
+ // Interpolate each property
823
+ for (const key of Object.keys(tw.to)) {
824
+ const start = tw.from[key];
825
+ const end = tw.to[key];
826
+ const value = start + (end - start) * t;
827
+ Tween.setProperty(tw.target, key, value);
828
+ }
829
+ tw.onUpdate?.(raw);
830
+ if (raw >= 1) {
831
+ completed.push(tw);
832
+ }
833
+ }
834
+ // Remove completed tweens
835
+ for (const tw of completed) {
836
+ const idx = Tween._tweens.indexOf(tw);
837
+ if (idx !== -1)
838
+ Tween._tweens.splice(idx, 1);
839
+ tw.resolve();
840
+ }
841
+ // Remove ticker when no active tweens
842
+ if (Tween._tweens.length === 0 && Tween._tickerAdded) {
843
+ pixi_js.Ticker.shared.remove(Tween.tick);
844
+ Tween._tickerAdded = false;
845
+ }
846
+ };
847
+ /**
848
+ * Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
849
+ */
850
+ static getProperty(target, key) {
851
+ const parts = key.split('.');
852
+ let obj = target;
853
+ for (let i = 0; i < parts.length - 1; i++) {
854
+ obj = obj[parts[i]];
855
+ }
856
+ return obj[parts[parts.length - 1]] ?? 0;
857
+ }
858
+ /**
859
+ * Set a potentially nested property.
860
+ */
861
+ static setProperty(target, key, value) {
862
+ const parts = key.split('.');
863
+ let obj = target;
864
+ for (let i = 0; i < parts.length - 1; i++) {
865
+ obj = obj[parts[i]];
866
+ }
867
+ obj[parts[parts.length - 1]] = value;
894
868
  }
895
869
  }
896
870
 
@@ -898,6 +872,8 @@ class WinDisplay extends pixi_js.Container {
898
872
  * Modal overlay component.
899
873
  * Shows content on top of a dark overlay with enter/exit animations.
900
874
  *
875
+ * The content container uses `@pixi/layout` for automatic centering.
876
+ *
901
877
  * @example
902
878
  * ```ts
903
879
  * const modal = new Modal({ closeOnOverlay: true });
@@ -916,11 +892,10 @@ class Modal extends pixi_js.Container {
916
892
  constructor(config = {}) {
917
893
  super();
918
894
  this._config = {
919
- overlayColor: 0x000000,
920
- overlayAlpha: 0.7,
921
- closeOnOverlay: true,
922
- animationDuration: 300,
923
- ...config,
895
+ overlayColor: config.overlayColor ?? 0x000000,
896
+ overlayAlpha: config.overlayAlpha ?? 0.7,
897
+ closeOnOverlay: config.closeOnOverlay ?? true,
898
+ animationDuration: config.animationDuration ?? 300,
924
899
  };
925
900
  // Overlay
926
901
  this._overlay = new pixi_js.Graphics();
@@ -988,6 +963,8 @@ const TOAST_COLORS = {
988
963
  /**
989
964
  * Toast notification component for displaying transient messages.
990
965
  *
966
+ * Uses `@pixi/layout` LayoutContainer for auto-sized background.
967
+ *
991
968
  * @example
992
969
  * ```ts
993
970
  * const toast = new Toast();
@@ -1003,11 +980,10 @@ class Toast extends pixi_js.Container {
1003
980
  constructor(config = {}) {
1004
981
  super();
1005
982
  this._config = {
1006
- duration: 3000,
1007
- bottomOffset: 60,
1008
- ...config,
983
+ duration: config.duration ?? 3000,
984
+ bottomOffset: config.bottomOffset ?? 60,
1009
985
  };
1010
- this._bg = new pixi_js.Graphics();
986
+ this._bg = new components.LayoutContainer();
1011
987
  this.addChild(this._bg);
1012
988
  this._text = new pixi_js.Text({
1013
989
  text: '',
@@ -1025,7 +1001,6 @@ class Toast extends pixi_js.Container {
1025
1001
  * Show a toast message.
1026
1002
  */
1027
1003
  async show(message, type = 'info', viewWidth, viewHeight) {
1028
- // Clear previous dismiss
1029
1004
  if (this._dismissTimeout) {
1030
1005
  clearTimeout(this._dismissTimeout);
1031
1006
  }
@@ -1034,10 +1009,16 @@ class Toast extends pixi_js.Container {
1034
1009
  const width = Math.max(200, this._text.width + padding * 2);
1035
1010
  const height = 44;
1036
1011
  const radius = 8;
1037
- this._bg.clear();
1038
- this._bg.roundRect(-width / 2, -height / 2, width, height, radius).fill(TOAST_COLORS[type]);
1039
- this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
1040
- .fill({ color: 0x000000, alpha: 0.2 });
1012
+ // Style the background
1013
+ this._bg.layout = {
1014
+ width,
1015
+ height,
1016
+ borderRadius: radius,
1017
+ backgroundColor: TOAST_COLORS[type],
1018
+ };
1019
+ // Center the bg around origin
1020
+ this._bg.x = -width / 2;
1021
+ this._bg.y = -height / 2;
1041
1022
  // Position
1042
1023
  if (viewWidth && viewHeight) {
1043
1024
  this.x = viewWidth / 2;
@@ -1046,9 +1027,7 @@ class Toast extends pixi_js.Container {
1046
1027
  this.visible = true;
1047
1028
  this.alpha = 0;
1048
1029
  this.y += 20;
1049
- // Animate in
1050
1030
  await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
1051
- // Auto-dismiss
1052
1031
  if (this._config.duration > 0) {
1053
1032
  this._dismissTimeout = setTimeout(() => {
1054
1033
  this.dismiss();
@@ -1070,12 +1049,320 @@ class Toast extends pixi_js.Container {
1070
1049
  }
1071
1050
  }
1072
1051
 
1052
+ // ─── Helpers ─────────────────────────────────────────────
1053
+ const ALIGNMENT_MAP = {
1054
+ start: 'flex-start',
1055
+ center: 'center',
1056
+ end: 'flex-end',
1057
+ stretch: 'stretch',
1058
+ };
1059
+ function normalizePadding(padding) {
1060
+ if (typeof padding === 'number')
1061
+ return [padding, padding, padding, padding];
1062
+ return padding;
1063
+ }
1064
+ function directionToFlexStyles(direction, maxWidth) {
1065
+ switch (direction) {
1066
+ case 'horizontal':
1067
+ return { flexDirection: 'row', flexWrap: 'nowrap' };
1068
+ case 'vertical':
1069
+ return { flexDirection: 'column', flexWrap: 'nowrap' };
1070
+ case 'grid':
1071
+ return { flexDirection: 'row', flexWrap: 'wrap' };
1072
+ case 'wrap':
1073
+ return {
1074
+ flexDirection: 'row',
1075
+ flexWrap: 'wrap',
1076
+ ...(maxWidth < Infinity ? { maxWidth } : {}),
1077
+ };
1078
+ }
1079
+ }
1080
+ function buildLayoutStyles(config) {
1081
+ const [pt, pr, pb, pl] = config.padding;
1082
+ return {
1083
+ ...directionToFlexStyles(config.direction, config.maxWidth),
1084
+ gap: config.gap,
1085
+ alignItems: ALIGNMENT_MAP[config.alignment],
1086
+ paddingTop: pt,
1087
+ paddingRight: pr,
1088
+ paddingBottom: pb,
1089
+ paddingLeft: pl,
1090
+ };
1091
+ }
1092
+ /**
1093
+ * Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
1094
+ *
1095
+ * Supports horizontal, vertical, grid, and wrap layout modes with
1096
+ * alignment, padding, gap, and viewport-anchor positioning.
1097
+ * Breakpoints allow different layouts for different screen sizes.
1098
+ *
1099
+ * @example
1100
+ * ```ts
1101
+ * const toolbar = new Layout({
1102
+ * direction: 'horizontal',
1103
+ * gap: 20,
1104
+ * alignment: 'center',
1105
+ * anchor: 'bottom-center',
1106
+ * padding: 16,
1107
+ * breakpoints: {
1108
+ * 768: { direction: 'vertical', gap: 10 },
1109
+ * },
1110
+ * });
1111
+ *
1112
+ * toolbar.addItem(spinButton);
1113
+ * toolbar.addItem(betLabel);
1114
+ * scene.container.addChild(toolbar);
1115
+ *
1116
+ * toolbar.updateViewport(width, height);
1117
+ * ```
1118
+ */
1119
+ class Layout extends pixi_js.Container {
1120
+ _layoutConfig;
1121
+ _padding;
1122
+ _anchor;
1123
+ _maxWidth;
1124
+ _breakpoints;
1125
+ _items = [];
1126
+ _viewportWidth = 0;
1127
+ _viewportHeight = 0;
1128
+ constructor(config = {}) {
1129
+ super();
1130
+ this._layoutConfig = {
1131
+ direction: config.direction ?? 'vertical',
1132
+ gap: config.gap ?? 0,
1133
+ alignment: config.alignment ?? 'start',
1134
+ autoLayout: config.autoLayout ?? true,
1135
+ columns: config.columns ?? 2,
1136
+ };
1137
+ this._padding = normalizePadding(config.padding ?? 0);
1138
+ this._anchor = config.anchor ?? 'top-left';
1139
+ this._maxWidth = config.maxWidth ?? Infinity;
1140
+ this._breakpoints = config.breakpoints
1141
+ ? Object.entries(config.breakpoints)
1142
+ .map(([w, cfg]) => [Number(w), cfg])
1143
+ .sort((a, b) => a[0] - b[0])
1144
+ : [];
1145
+ this.applyLayoutStyles();
1146
+ }
1147
+ /** Add an item to the layout */
1148
+ addItem(child) {
1149
+ this._items.push(child);
1150
+ this.addChild(child);
1151
+ if (this._layoutConfig.direction === 'grid') {
1152
+ this.applyGridChildWidth(child);
1153
+ }
1154
+ return this;
1155
+ }
1156
+ /** Remove an item from the layout */
1157
+ removeItem(child) {
1158
+ const idx = this._items.indexOf(child);
1159
+ if (idx !== -1) {
1160
+ this._items.splice(idx, 1);
1161
+ this.removeChild(child);
1162
+ }
1163
+ return this;
1164
+ }
1165
+ /** Remove all items */
1166
+ clearItems() {
1167
+ for (const item of this._items) {
1168
+ this.removeChild(item);
1169
+ }
1170
+ this._items.length = 0;
1171
+ return this;
1172
+ }
1173
+ /** Get all layout items */
1174
+ get items() {
1175
+ return this._items;
1176
+ }
1177
+ /**
1178
+ * Update the viewport size and recalculate layout.
1179
+ * Should be called from `Scene.onResize()`.
1180
+ */
1181
+ updateViewport(width, height) {
1182
+ this._viewportWidth = width;
1183
+ this._viewportHeight = height;
1184
+ this.applyLayoutStyles();
1185
+ this.applyAnchor();
1186
+ }
1187
+ applyLayoutStyles() {
1188
+ const effective = this.resolveConfig();
1189
+ const direction = effective.direction ?? this._layoutConfig.direction;
1190
+ const gap = effective.gap ?? this._layoutConfig.gap;
1191
+ const alignment = effective.alignment ?? this._layoutConfig.alignment;
1192
+ effective.columns ?? this._layoutConfig.columns;
1193
+ const padding = effective.padding !== undefined
1194
+ ? normalizePadding(effective.padding)
1195
+ : this._padding;
1196
+ const maxWidth = effective.maxWidth ?? this._maxWidth;
1197
+ const styles = buildLayoutStyles({ direction, gap, alignment, padding, maxWidth });
1198
+ this.layout = styles;
1199
+ if (direction === 'grid') {
1200
+ for (const item of this._items) {
1201
+ this.applyGridChildWidth(item);
1202
+ }
1203
+ }
1204
+ }
1205
+ applyGridChildWidth(child) {
1206
+ const effective = this.resolveConfig();
1207
+ const columns = effective.columns ?? this._layoutConfig.columns;
1208
+ const pct = `${(100 / columns).toFixed(2)}%`;
1209
+ if (child._layout) {
1210
+ child._layout.setStyle({ width: pct });
1211
+ }
1212
+ else {
1213
+ child.layout = { width: pct };
1214
+ }
1215
+ }
1216
+ applyAnchor() {
1217
+ const anchor = this.resolveConfig().anchor ?? this._anchor;
1218
+ if (this._viewportWidth === 0 || this._viewportHeight === 0)
1219
+ return;
1220
+ const bounds = this.getLocalBounds();
1221
+ const contentW = bounds.width * this.scale.x;
1222
+ const contentH = bounds.height * this.scale.y;
1223
+ const vw = this._viewportWidth;
1224
+ const vh = this._viewportHeight;
1225
+ let anchorX = 0;
1226
+ let anchorY = 0;
1227
+ if (anchor.includes('left')) {
1228
+ anchorX = 0;
1229
+ }
1230
+ else if (anchor.includes('right')) {
1231
+ anchorX = vw - contentW;
1232
+ }
1233
+ else {
1234
+ anchorX = (vw - contentW) / 2;
1235
+ }
1236
+ if (anchor.startsWith('top')) {
1237
+ anchorY = 0;
1238
+ }
1239
+ else if (anchor.startsWith('bottom')) {
1240
+ anchorY = vh - contentH;
1241
+ }
1242
+ else {
1243
+ anchorY = (vh - contentH) / 2;
1244
+ }
1245
+ this.x = anchorX - bounds.x * this.scale.x;
1246
+ this.y = anchorY - bounds.y * this.scale.y;
1247
+ }
1248
+ resolveConfig() {
1249
+ if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
1250
+ return {};
1251
+ }
1252
+ for (const [maxWidth, overrides] of this._breakpoints) {
1253
+ if (this._viewportWidth <= maxWidth) {
1254
+ return overrides;
1255
+ }
1256
+ }
1257
+ return {};
1258
+ }
1259
+ }
1260
+
1261
+ const DIRECTION_MAP = {
1262
+ vertical: 'vertical',
1263
+ horizontal: 'horizontal',
1264
+ both: 'bidirectional',
1265
+ };
1266
+ /**
1267
+ * Scrollable container powered by `@pixi/ui` ScrollBox.
1268
+ *
1269
+ * Provides touch/drag scrolling, mouse wheel support, inertia, and
1270
+ * dynamic rendering optimization for off-screen items.
1271
+ *
1272
+ * @example
1273
+ * ```ts
1274
+ * const scroll = new ScrollContainer({
1275
+ * width: 600,
1276
+ * height: 400,
1277
+ * direction: 'vertical',
1278
+ * elementsMargin: 8,
1279
+ * });
1280
+ *
1281
+ * for (let i = 0; i < 50; i++) {
1282
+ * scroll.addItem(createRow(i));
1283
+ * }
1284
+ *
1285
+ * scene.container.addChild(scroll);
1286
+ * ```
1287
+ */
1288
+ class ScrollContainer extends ui.ScrollBox {
1289
+ _scrollConfig;
1290
+ constructor(config) {
1291
+ const options = {
1292
+ width: config.width,
1293
+ height: config.height,
1294
+ type: DIRECTION_MAP[config.direction ?? 'vertical'],
1295
+ radius: config.borderRadius ?? 0,
1296
+ elementsMargin: config.elementsMargin ?? 0,
1297
+ padding: config.padding ?? 0,
1298
+ disableDynamicRendering: config.disableDynamicRendering ?? false,
1299
+ disableEasing: config.disableEasing ?? false,
1300
+ globalScroll: config.globalScroll ?? true,
1301
+ };
1302
+ if (config.backgroundColor !== undefined) {
1303
+ options.background = config.backgroundColor;
1304
+ }
1305
+ super(options);
1306
+ this._scrollConfig = config;
1307
+ }
1308
+ /** Set scrollable content. Replaces any existing content. */
1309
+ setContent(content) {
1310
+ // Remove existing items
1311
+ const existing = this.items;
1312
+ if (existing.length > 0) {
1313
+ for (let i = existing.length - 1; i >= 0; i--) {
1314
+ this.removeItem(i);
1315
+ }
1316
+ }
1317
+ // Add all children from the content container
1318
+ const children = [...content.children];
1319
+ if (children.length > 0) {
1320
+ this.addItems(children);
1321
+ }
1322
+ }
1323
+ /** Add a single item */
1324
+ addItem(...items) {
1325
+ this.addItems(items);
1326
+ return items[0];
1327
+ }
1328
+ /** Scroll to make a specific item/child visible */
1329
+ scrollToItem(index) {
1330
+ this.scrollTo(index);
1331
+ }
1332
+ /** Current scroll position */
1333
+ get scrollPosition() {
1334
+ return { x: this.scrollX, y: this.scrollY };
1335
+ }
1336
+ }
1337
+
1338
+ Object.defineProperty(exports, "PixiLayout", {
1339
+ enumerable: true,
1340
+ get: function () { return layout.Layout; }
1341
+ });
1342
+ Object.defineProperty(exports, "ButtonContainer", {
1343
+ enumerable: true,
1344
+ get: function () { return ui.ButtonContainer; }
1345
+ });
1346
+ Object.defineProperty(exports, "FancyButton", {
1347
+ enumerable: true,
1348
+ get: function () { return ui.FancyButton; }
1349
+ });
1350
+ Object.defineProperty(exports, "ScrollBox", {
1351
+ enumerable: true,
1352
+ get: function () { return ui.ScrollBox; }
1353
+ });
1354
+ Object.defineProperty(exports, "LayoutContainer", {
1355
+ enumerable: true,
1356
+ get: function () { return components.LayoutContainer; }
1357
+ });
1073
1358
  exports.BalanceDisplay = BalanceDisplay;
1074
1359
  exports.Button = Button;
1075
1360
  exports.Label = Label;
1361
+ exports.Layout = Layout;
1076
1362
  exports.Modal = Modal;
1077
1363
  exports.Panel = Panel;
1078
1364
  exports.ProgressBar = ProgressBar;
1365
+ exports.ScrollContainer = ScrollContainer;
1079
1366
  exports.Toast = Toast;
1080
1367
  exports.WinDisplay = WinDisplay;
1081
1368
  //# sourceMappingURL=ui.cjs.js.map