@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.
- package/README.md +400 -35
- package/dist/animation.cjs.js +191 -1
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.d.ts +117 -1
- package/dist/animation.esm.js +192 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/audio.cjs.js +66 -16
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +4 -0
- package/dist/audio.esm.js +66 -16
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +307 -85
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +60 -1
- package/dist/core.esm.js +308 -86
- package/dist/core.esm.js.map +1 -1
- package/dist/debug.cjs.js +36 -68
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +4 -6
- package/dist/debug.esm.js +36 -68
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +997 -475
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +356 -79
- package/dist/index.esm.js +983 -478
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +816 -529
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +179 -41
- package/dist/ui.esm.js +798 -531
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +85 -68
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +17 -23
- package/dist/vite.esm.js +86 -68
- package/dist/vite.esm.js.map +1 -1
- package/package.json +19 -5
- package/src/animation/SpriteAnimation.ts +210 -0
- package/src/animation/Tween.ts +27 -1
- package/src/animation/index.ts +2 -0
- package/src/audio/AudioManager.ts +64 -15
- package/src/core/EventEmitter.ts +7 -1
- package/src/core/GameApplication.ts +19 -7
- package/src/core/SceneManager.ts +3 -1
- package/src/debug/DevBridge.ts +49 -80
- package/src/index.ts +22 -0
- package/src/input/InputManager.ts +26 -0
- package/src/loading/CSSPreloader.ts +7 -33
- package/src/loading/LoadingScene.ts +17 -41
- package/src/loading/index.ts +1 -0
- package/src/loading/logo.ts +95 -0
- package/src/types.ts +4 -0
- package/src/ui/BalanceDisplay.ts +12 -1
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +286 -0
- package/src/ui/Modal.ts +6 -5
- package/src/ui/Panel.ts +52 -55
- package/src/ui/ProgressBar.ts +52 -57
- package/src/ui/ScrollContainer.ts +126 -0
- package/src/ui/Toast.ts +19 -13
- package/src/ui/index.ts +17 -0
- package/src/viewport/ViewportManager.ts +2 -0
- package/src/vite/index.ts +103 -83
package/dist/ui.esm.js
CHANGED
|
@@ -1,409 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
const Easing = {
|
|
9
|
-
linear: (t) => t,
|
|
10
|
-
easeInQuad: (t) => t * t,
|
|
11
|
-
easeOutQuad: (t) => t * (2 - t),
|
|
12
|
-
easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
|
13
|
-
easeInCubic: (t) => t * t * t,
|
|
14
|
-
easeOutCubic: (t) => --t * t * t + 1,
|
|
15
|
-
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
|
|
16
|
-
easeInQuart: (t) => t * t * t * t,
|
|
17
|
-
easeOutQuart: (t) => 1 - --t * t * t * t,
|
|
18
|
-
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
|
|
19
|
-
easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
|
|
20
|
-
easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
|
|
21
|
-
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
22
|
-
easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
|
|
23
|
-
easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
|
|
24
|
-
easeInOutExpo: (t) => t === 0
|
|
25
|
-
? 0
|
|
26
|
-
: t === 1
|
|
27
|
-
? 1
|
|
28
|
-
: t < 0.5
|
|
29
|
-
? Math.pow(2, 20 * t - 10) / 2
|
|
30
|
-
: (2 - Math.pow(2, -20 * t + 10)) / 2,
|
|
31
|
-
easeInBack: (t) => {
|
|
32
|
-
const c1 = 1.70158;
|
|
33
|
-
const c3 = c1 + 1;
|
|
34
|
-
return c3 * t * t * t - c1 * t * t;
|
|
35
|
-
},
|
|
36
|
-
easeOutBack: (t) => {
|
|
37
|
-
const c1 = 1.70158;
|
|
38
|
-
const c3 = c1 + 1;
|
|
39
|
-
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
40
|
-
},
|
|
41
|
-
easeInOutBack: (t) => {
|
|
42
|
-
const c1 = 1.70158;
|
|
43
|
-
const c2 = c1 * 1.525;
|
|
44
|
-
return t < 0.5
|
|
45
|
-
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
|
46
|
-
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
|
|
47
|
-
},
|
|
48
|
-
easeOutBounce: (t) => {
|
|
49
|
-
const n1 = 7.5625;
|
|
50
|
-
const d1 = 2.75;
|
|
51
|
-
if (t < 1 / d1)
|
|
52
|
-
return n1 * t * t;
|
|
53
|
-
if (t < 2 / d1)
|
|
54
|
-
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
55
|
-
if (t < 2.5 / d1)
|
|
56
|
-
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
57
|
-
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
58
|
-
},
|
|
59
|
-
easeInBounce: (t) => 1 - Easing.easeOutBounce(1 - t),
|
|
60
|
-
easeInOutBounce: (t) => t < 0.5
|
|
61
|
-
? (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
|
|
62
|
-
: (1 + Easing.easeOutBounce(2 * t - 1)) / 2,
|
|
63
|
-
easeOutElastic: (t) => {
|
|
64
|
-
const c4 = (2 * Math.PI) / 3;
|
|
65
|
-
return t === 0
|
|
66
|
-
? 0
|
|
67
|
-
: t === 1
|
|
68
|
-
? 1
|
|
69
|
-
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
|
70
|
-
},
|
|
71
|
-
easeInElastic: (t) => {
|
|
72
|
-
const c4 = (2 * Math.PI) / 3;
|
|
73
|
-
return t === 0
|
|
74
|
-
? 0
|
|
75
|
-
: t === 1
|
|
76
|
-
? 1
|
|
77
|
-
: -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Lightweight tween system integrated with PixiJS Ticker.
|
|
83
|
-
* Zero external dependencies — no GSAP required.
|
|
84
|
-
*
|
|
85
|
-
* All tweens return a Promise that resolves on completion.
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```ts
|
|
89
|
-
* // Fade in a sprite
|
|
90
|
-
* await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
|
|
91
|
-
*
|
|
92
|
-
* // Move and wait
|
|
93
|
-
* await Tween.to(sprite, { x: 500 }, 300);
|
|
94
|
-
*
|
|
95
|
-
* // From a starting value
|
|
96
|
-
* await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
class Tween {
|
|
100
|
-
static _tweens = [];
|
|
101
|
-
static _tickerAdded = false;
|
|
102
|
-
/**
|
|
103
|
-
* Animate properties from current values to target values.
|
|
104
|
-
*
|
|
105
|
-
* @param target - Object to animate (Sprite, Container, etc.)
|
|
106
|
-
* @param props - Target property values
|
|
107
|
-
* @param duration - Duration in milliseconds
|
|
108
|
-
* @param easing - Easing function (default: easeOutQuad)
|
|
109
|
-
* @param onUpdate - Progress callback (0..1)
|
|
110
|
-
*/
|
|
111
|
-
static to(target, props, duration, easing, onUpdate) {
|
|
112
|
-
return new Promise((resolve) => {
|
|
113
|
-
// Capture starting values
|
|
114
|
-
const from = {};
|
|
115
|
-
for (const key of Object.keys(props)) {
|
|
116
|
-
from[key] = Tween.getProperty(target, key);
|
|
117
|
-
}
|
|
118
|
-
const tween = {
|
|
119
|
-
target,
|
|
120
|
-
from,
|
|
121
|
-
to: { ...props },
|
|
122
|
-
duration: Math.max(1, duration),
|
|
123
|
-
easing: easing ?? Easing.easeOutQuad,
|
|
124
|
-
elapsed: 0,
|
|
125
|
-
delay: 0,
|
|
126
|
-
resolve,
|
|
127
|
-
onUpdate,
|
|
128
|
-
};
|
|
129
|
-
Tween._tweens.push(tween);
|
|
130
|
-
Tween.ensureTicker();
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Animate properties from given values to current values.
|
|
135
|
-
*/
|
|
136
|
-
static from(target, props, duration, easing, onUpdate) {
|
|
137
|
-
// Capture current values as "to"
|
|
138
|
-
const to = {};
|
|
139
|
-
for (const key of Object.keys(props)) {
|
|
140
|
-
to[key] = Tween.getProperty(target, key);
|
|
141
|
-
Tween.setProperty(target, key, props[key]);
|
|
142
|
-
}
|
|
143
|
-
return Tween.to(target, to, duration, easing, onUpdate);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Animate from one set of values to another.
|
|
147
|
-
*/
|
|
148
|
-
static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
|
|
149
|
-
// Set starting values
|
|
150
|
-
for (const key of Object.keys(fromProps)) {
|
|
151
|
-
Tween.setProperty(target, key, fromProps[key]);
|
|
152
|
-
}
|
|
153
|
-
return Tween.to(target, toProps, duration, easing, onUpdate);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Wait for a given duration (useful in timelines).
|
|
157
|
-
*/
|
|
158
|
-
static delay(ms) {
|
|
159
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Kill all tweens on a target.
|
|
163
|
-
*/
|
|
164
|
-
static killTweensOf(target) {
|
|
165
|
-
Tween._tweens = Tween._tweens.filter((tw) => {
|
|
166
|
-
if (tw.target === target) {
|
|
167
|
-
tw.resolve();
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
return true;
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Kill all active tweens.
|
|
175
|
-
*/
|
|
176
|
-
static killAll() {
|
|
177
|
-
for (const tw of Tween._tweens) {
|
|
178
|
-
tw.resolve();
|
|
179
|
-
}
|
|
180
|
-
Tween._tweens.length = 0;
|
|
181
|
-
}
|
|
182
|
-
/** Number of active tweens */
|
|
183
|
-
static get activeTweens() {
|
|
184
|
-
return Tween._tweens.length;
|
|
185
|
-
}
|
|
186
|
-
// ─── Internal ──────────────────────────────────────────
|
|
187
|
-
static ensureTicker() {
|
|
188
|
-
if (Tween._tickerAdded)
|
|
189
|
-
return;
|
|
190
|
-
Tween._tickerAdded = true;
|
|
191
|
-
Ticker.shared.add(Tween.tick);
|
|
192
|
-
}
|
|
193
|
-
static tick = (ticker) => {
|
|
194
|
-
const dt = ticker.deltaMS;
|
|
195
|
-
const completed = [];
|
|
196
|
-
for (const tw of Tween._tweens) {
|
|
197
|
-
tw.elapsed += dt;
|
|
198
|
-
if (tw.elapsed < tw.delay)
|
|
199
|
-
continue;
|
|
200
|
-
const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
|
|
201
|
-
const t = tw.easing(raw);
|
|
202
|
-
// Interpolate each property
|
|
203
|
-
for (const key of Object.keys(tw.to)) {
|
|
204
|
-
const start = tw.from[key];
|
|
205
|
-
const end = tw.to[key];
|
|
206
|
-
const value = start + (end - start) * t;
|
|
207
|
-
Tween.setProperty(tw.target, key, value);
|
|
208
|
-
}
|
|
209
|
-
tw.onUpdate?.(raw);
|
|
210
|
-
if (raw >= 1) {
|
|
211
|
-
completed.push(tw);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Remove completed tweens
|
|
215
|
-
for (const tw of completed) {
|
|
216
|
-
const idx = Tween._tweens.indexOf(tw);
|
|
217
|
-
if (idx !== -1)
|
|
218
|
-
Tween._tweens.splice(idx, 1);
|
|
219
|
-
tw.resolve();
|
|
220
|
-
}
|
|
221
|
-
// Remove ticker when no active tweens
|
|
222
|
-
if (Tween._tweens.length === 0 && Tween._tickerAdded) {
|
|
223
|
-
Ticker.shared.remove(Tween.tick);
|
|
224
|
-
Tween._tickerAdded = false;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
/**
|
|
228
|
-
* Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
|
|
229
|
-
*/
|
|
230
|
-
static getProperty(target, key) {
|
|
231
|
-
const parts = key.split('.');
|
|
232
|
-
let obj = target;
|
|
233
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
234
|
-
obj = obj[parts[i]];
|
|
235
|
-
}
|
|
236
|
-
return obj[parts[parts.length - 1]] ?? 0;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Set a potentially nested property.
|
|
240
|
-
*/
|
|
241
|
-
static setProperty(target, key, value) {
|
|
242
|
-
const parts = key.split('.');
|
|
243
|
-
let obj = target;
|
|
244
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
245
|
-
obj = obj[parts[i]];
|
|
246
|
-
}
|
|
247
|
-
obj[parts[parts.length - 1]] = value;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
1
|
+
export { Layout as PixiLayout } from '@pixi/layout';
|
|
2
|
+
import { Graphics, Container, Text, Texture, NineSliceSprite, Ticker } from 'pixi.js';
|
|
3
|
+
import { FancyButton, ProgressBar as ProgressBar$1, ScrollBox } from '@pixi/ui';
|
|
4
|
+
export { ButtonContainer, FancyButton, ScrollBox } from '@pixi/ui';
|
|
5
|
+
import { LayoutContainer } from '@pixi/layout/components';
|
|
6
|
+
export { LayoutContainer } from '@pixi/layout/components';
|
|
250
7
|
|
|
251
8
|
const DEFAULT_COLORS = {
|
|
252
|
-
|
|
9
|
+
default: 0xffd700,
|
|
253
10
|
hover: 0xffe44d,
|
|
254
11
|
pressed: 0xccac00,
|
|
255
12
|
disabled: 0x666666,
|
|
256
13
|
};
|
|
14
|
+
function makeGraphicsView(w, h, radius, color) {
|
|
15
|
+
const g = new 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
|
+
}
|
|
257
21
|
/**
|
|
258
|
-
* Interactive button component
|
|
22
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
259
23
|
*
|
|
260
|
-
* 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.
|
|
261
26
|
*
|
|
262
27
|
* @example
|
|
263
28
|
* ```ts
|
|
264
29
|
* const btn = new Button({
|
|
265
30
|
* width: 200, height: 60, borderRadius: 12,
|
|
266
|
-
* colors: {
|
|
31
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
32
|
+
* text: 'SPIN',
|
|
267
33
|
* });
|
|
268
34
|
*
|
|
269
|
-
* btn.
|
|
35
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
270
36
|
* scene.container.addChild(btn);
|
|
271
37
|
* ```
|
|
272
38
|
*/
|
|
273
|
-
class Button extends
|
|
274
|
-
|
|
275
|
-
_bg;
|
|
276
|
-
_sprites = {};
|
|
277
|
-
_config;
|
|
278
|
-
/** Called when the button is tapped/clicked */
|
|
279
|
-
onTap;
|
|
280
|
-
/** Called when the button state changes */
|
|
281
|
-
onStateChange;
|
|
39
|
+
class Button extends FancyButton {
|
|
40
|
+
_buttonConfig;
|
|
282
41
|
constructor(config = {}) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
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,
|
|
290
48
|
...config,
|
|
291
49
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
296
67
|
if (config.textures) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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;
|
|
305
87
|
}
|
|
306
|
-
|
|
307
|
-
this.
|
|
308
|
-
this.cursor = 'pointer';
|
|
309
|
-
// Set up hit area for Graphics-based
|
|
310
|
-
this.pivot.set(this._config.width / 2, this._config.height / 2);
|
|
311
|
-
// Bind events
|
|
312
|
-
this.on('pointerover', this.onPointerOver);
|
|
313
|
-
this.on('pointerout', this.onPointerOut);
|
|
314
|
-
this.on('pointerdown', this.onPointerDown);
|
|
315
|
-
this.on('pointerup', this.onPointerUp);
|
|
316
|
-
this.on('pointertap', this.onPointerTap);
|
|
317
|
-
// Initial render
|
|
318
|
-
this.setState('normal');
|
|
88
|
+
super(options);
|
|
89
|
+
this._buttonConfig = resolvedConfig;
|
|
319
90
|
if (config.disabled) {
|
|
320
|
-
this.
|
|
91
|
+
this.enabled = false;
|
|
321
92
|
}
|
|
322
93
|
}
|
|
323
|
-
/** Current button state */
|
|
324
|
-
get state() {
|
|
325
|
-
return this._state;
|
|
326
|
-
}
|
|
327
94
|
/** Enable the button */
|
|
328
95
|
enable() {
|
|
329
|
-
|
|
330
|
-
this.setState('normal');
|
|
331
|
-
this.eventMode = 'static';
|
|
332
|
-
this.cursor = 'pointer';
|
|
333
|
-
}
|
|
96
|
+
this.enabled = true;
|
|
334
97
|
}
|
|
335
98
|
/** Disable the button */
|
|
336
99
|
disable() {
|
|
337
|
-
this.
|
|
338
|
-
this.eventMode = 'none';
|
|
339
|
-
this.cursor = 'default';
|
|
100
|
+
this.enabled = false;
|
|
340
101
|
}
|
|
341
102
|
/** Whether the button is disabled */
|
|
342
103
|
get disabled() {
|
|
343
|
-
return this.
|
|
104
|
+
return !this.enabled;
|
|
344
105
|
}
|
|
345
|
-
setState(state) {
|
|
346
|
-
if (this._state === state)
|
|
347
|
-
return;
|
|
348
|
-
this._state = state;
|
|
349
|
-
this.render();
|
|
350
|
-
this.onStateChange?.(state);
|
|
351
|
-
}
|
|
352
|
-
render() {
|
|
353
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
354
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
355
|
-
// Update Graphics
|
|
356
|
-
this._bg.clear();
|
|
357
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
358
|
-
// Add highlight for normal/hover
|
|
359
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
360
|
-
this._bg
|
|
361
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
362
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
363
|
-
}
|
|
364
|
-
// Update sprite visibility
|
|
365
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
366
|
-
if (sprite)
|
|
367
|
-
sprite.visible = state === this._state;
|
|
368
|
-
}
|
|
369
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
370
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
371
|
-
this._sprites.normal.visible = true;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
onPointerOver = () => {
|
|
375
|
-
if (this._state === 'disabled')
|
|
376
|
-
return;
|
|
377
|
-
this.setState('hover');
|
|
378
|
-
};
|
|
379
|
-
onPointerOut = () => {
|
|
380
|
-
if (this._state === 'disabled')
|
|
381
|
-
return;
|
|
382
|
-
this.setState('normal');
|
|
383
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
384
|
-
};
|
|
385
|
-
onPointerDown = () => {
|
|
386
|
-
if (this._state === 'disabled')
|
|
387
|
-
return;
|
|
388
|
-
this.setState('pressed');
|
|
389
|
-
const s = this._config.pressScale;
|
|
390
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
391
|
-
};
|
|
392
|
-
onPointerUp = () => {
|
|
393
|
-
if (this._state === 'disabled')
|
|
394
|
-
return;
|
|
395
|
-
this.setState('hover');
|
|
396
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
397
|
-
};
|
|
398
|
-
onPointerTap = () => {
|
|
399
|
-
if (this._state === 'disabled')
|
|
400
|
-
return;
|
|
401
|
-
this.onTap?.();
|
|
402
|
-
};
|
|
403
106
|
}
|
|
404
107
|
|
|
108
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
109
|
+
return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
110
|
+
}
|
|
405
111
|
/**
|
|
406
|
-
* Horizontal progress bar
|
|
112
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
113
|
+
*
|
|
114
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
407
115
|
*
|
|
408
116
|
* @example
|
|
409
117
|
* ```ts
|
|
@@ -413,33 +121,48 @@ class Button extends Container {
|
|
|
413
121
|
* ```
|
|
414
122
|
*/
|
|
415
123
|
class ProgressBar extends Container {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
_border;
|
|
124
|
+
_bar;
|
|
125
|
+
_borderGfx;
|
|
419
126
|
_config;
|
|
420
127
|
_progress = 0;
|
|
421
128
|
_displayedProgress = 0;
|
|
422
129
|
constructor(config = {}) {
|
|
423
130
|
super();
|
|
424
131
|
this._config = {
|
|
425
|
-
width: 300,
|
|
426
|
-
height: 16,
|
|
427
|
-
borderRadius: 8,
|
|
428
|
-
fillColor: 0xffd700,
|
|
429
|
-
trackColor: 0x333333,
|
|
430
|
-
borderColor: 0x555555,
|
|
431
|
-
borderWidth: 1,
|
|
432
|
-
animated: true,
|
|
433
|
-
animationSpeed: 0.1,
|
|
434
|
-
|
|
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,
|
|
435
155
|
};
|
|
436
|
-
this.
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
156
|
+
this._bar = new ProgressBar$1(options);
|
|
157
|
+
this.addChild(this._bar);
|
|
158
|
+
// Border overlay
|
|
159
|
+
this._borderGfx = new 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);
|
|
443
166
|
}
|
|
444
167
|
/** Get/set progress (0..1) */
|
|
445
168
|
get progress() {
|
|
@@ -449,13 +172,13 @@ class ProgressBar extends Container {
|
|
|
449
172
|
this._progress = Math.max(0, Math.min(1, value));
|
|
450
173
|
if (!this._config.animated) {
|
|
451
174
|
this._displayedProgress = this._progress;
|
|
452
|
-
this.
|
|
175
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
453
176
|
}
|
|
454
177
|
}
|
|
455
178
|
/**
|
|
456
179
|
* Call each frame if animated is true.
|
|
457
180
|
*/
|
|
458
|
-
update(
|
|
181
|
+
update(_dt) {
|
|
459
182
|
if (!this._config.animated)
|
|
460
183
|
return;
|
|
461
184
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -464,35 +187,7 @@ class ProgressBar extends Container {
|
|
|
464
187
|
}
|
|
465
188
|
this._displayedProgress +=
|
|
466
189
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
467
|
-
this.
|
|
468
|
-
}
|
|
469
|
-
drawTrack() {
|
|
470
|
-
const { width, height, borderRadius, trackColor } = this._config;
|
|
471
|
-
this._track.clear();
|
|
472
|
-
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
473
|
-
}
|
|
474
|
-
drawBorder() {
|
|
475
|
-
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
476
|
-
this._border.clear();
|
|
477
|
-
this._border
|
|
478
|
-
.roundRect(0, 0, width, height, borderRadius)
|
|
479
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
480
|
-
}
|
|
481
|
-
drawFill(progress) {
|
|
482
|
-
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
483
|
-
const innerWidth = width - borderWidth * 2;
|
|
484
|
-
const innerHeight = height - borderWidth * 2;
|
|
485
|
-
const fillWidth = Math.max(0, innerWidth * progress);
|
|
486
|
-
this._fill.clear();
|
|
487
|
-
if (fillWidth > 0) {
|
|
488
|
-
this._fill.x = borderWidth;
|
|
489
|
-
this._fill.y = borderWidth;
|
|
490
|
-
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
491
|
-
// Highlight
|
|
492
|
-
this._fill
|
|
493
|
-
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
494
|
-
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
495
|
-
}
|
|
190
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
496
191
|
}
|
|
497
192
|
}
|
|
498
193
|
|
|
@@ -588,7 +283,10 @@ class Label extends Container {
|
|
|
588
283
|
}
|
|
589
284
|
|
|
590
285
|
/**
|
|
591
|
-
* Background panel
|
|
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.
|
|
592
290
|
*
|
|
593
291
|
* @example
|
|
594
292
|
* ```ts
|
|
@@ -603,75 +301,148 @@ class Label extends Container {
|
|
|
603
301
|
* });
|
|
604
302
|
* ```
|
|
605
303
|
*/
|
|
606
|
-
class Panel extends
|
|
607
|
-
|
|
608
|
-
_content;
|
|
609
|
-
_config;
|
|
304
|
+
class Panel extends LayoutContainer {
|
|
305
|
+
_panelConfig;
|
|
610
306
|
constructor(config = {}) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
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,
|
|
617
312
|
...config,
|
|
618
313
|
};
|
|
619
|
-
//
|
|
314
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
315
|
+
let customBackground;
|
|
620
316
|
if (config.nineSliceTexture) {
|
|
621
317
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
622
318
|
? Texture.from(config.nineSliceTexture)
|
|
623
319
|
: config.nineSliceTexture;
|
|
624
320
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
625
|
-
|
|
321
|
+
const nineSlice = new NineSliceSprite({
|
|
626
322
|
texture,
|
|
627
323
|
leftWidth: left,
|
|
628
324
|
topHeight: top,
|
|
629
325
|
rightWidth: right,
|
|
630
326
|
bottomHeight: bottom,
|
|
631
327
|
});
|
|
632
|
-
|
|
633
|
-
|
|
328
|
+
nineSlice.width = resolvedConfig.width;
|
|
329
|
+
nineSlice.height = resolvedConfig.height;
|
|
330
|
+
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
331
|
+
customBackground = nineSlice;
|
|
634
332
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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;
|
|
638
354
|
}
|
|
639
|
-
this._bg.alpha = this._config.backgroundAlpha;
|
|
640
|
-
this.addChild(this._bg);
|
|
641
|
-
// Content container with padding
|
|
642
|
-
this._content = new Container();
|
|
643
|
-
this._content.x = this._config.padding;
|
|
644
|
-
this._content.y = this._config.padding;
|
|
645
|
-
this.addChild(this._content);
|
|
646
355
|
}
|
|
647
|
-
/**
|
|
356
|
+
/** Access the content container (children added here participate in layout) */
|
|
648
357
|
get content() {
|
|
649
|
-
return this.
|
|
358
|
+
return this.overflowContainer;
|
|
650
359
|
}
|
|
651
360
|
/** Resize the panel */
|
|
652
361
|
setSize(width, height) {
|
|
653
|
-
this.
|
|
654
|
-
this.
|
|
655
|
-
|
|
656
|
-
this.drawGraphicsBg();
|
|
657
|
-
}
|
|
658
|
-
else {
|
|
659
|
-
this._bg.width = width;
|
|
660
|
-
this._bg.height = height;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
drawGraphicsBg() {
|
|
664
|
-
const bg = this._bg;
|
|
665
|
-
const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
|
|
666
|
-
bg.clear();
|
|
667
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
668
|
-
if (borderColor !== undefined && borderWidth) {
|
|
669
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0)
|
|
670
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
671
|
-
}
|
|
362
|
+
this._panelConfig.width = width;
|
|
363
|
+
this._panelConfig.height = height;
|
|
364
|
+
this._layout?.setStyle({ width, height });
|
|
672
365
|
}
|
|
673
366
|
}
|
|
674
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
|
+
|
|
675
446
|
/**
|
|
676
447
|
* Reactive balance display component.
|
|
677
448
|
*
|
|
@@ -694,6 +465,7 @@ class BalanceDisplay extends Container {
|
|
|
694
465
|
_currentValue = 0;
|
|
695
466
|
_displayedValue = 0;
|
|
696
467
|
_animating = false;
|
|
468
|
+
_animationCancelled = false;
|
|
697
469
|
constructor(config = {}) {
|
|
698
470
|
super();
|
|
699
471
|
this._config = {
|
|
@@ -755,11 +527,20 @@ class BalanceDisplay extends Container {
|
|
|
755
527
|
this.updateDisplay();
|
|
756
528
|
}
|
|
757
529
|
async animateValue(from, to) {
|
|
530
|
+
if (this._animating) {
|
|
531
|
+
this._animationCancelled = true;
|
|
532
|
+
}
|
|
758
533
|
this._animating = true;
|
|
534
|
+
this._animationCancelled = false;
|
|
759
535
|
const duration = this._config.animationDuration;
|
|
760
536
|
const startTime = Date.now();
|
|
761
537
|
return new Promise((resolve) => {
|
|
762
538
|
const tick = () => {
|
|
539
|
+
if (this._animationCancelled) {
|
|
540
|
+
this._animating = false;
|
|
541
|
+
resolve();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
763
544
|
const elapsed = Date.now() - startTime;
|
|
764
545
|
const t = Math.min(elapsed / duration, 1);
|
|
765
546
|
const eased = Easing.easeOutCubic(t);
|
|
@@ -825,70 +606,265 @@ class WinDisplay extends Container {
|
|
|
825
606
|
...config.style,
|
|
826
607
|
},
|
|
827
608
|
});
|
|
828
|
-
this.addChild(this._label);
|
|
829
|
-
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
|
+
});
|
|
830
727
|
}
|
|
831
728
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* @param amount - Win amount
|
|
835
|
-
* @returns Promise that resolves when the animation completes
|
|
729
|
+
* Animate properties from given values to current values.
|
|
836
730
|
*/
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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) {
|
|
845
755
|
return new Promise((resolve) => {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
const elapsed = Date.now() - startTime;
|
|
853
|
-
const t = Math.min(elapsed / duration, 1);
|
|
854
|
-
const eased = Easing.easeOutCubic(t);
|
|
855
|
-
// Countup
|
|
856
|
-
const current = amount * eased;
|
|
857
|
-
this.displayAmount(current);
|
|
858
|
-
// Scale animation
|
|
859
|
-
const scaleT = Math.min(elapsed / 300, 1);
|
|
860
|
-
const scaleEased = Easing.easeOutBack(scaleT);
|
|
861
|
-
const targetScale = 1;
|
|
862
|
-
this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
|
|
863
|
-
if (t < 1) {
|
|
864
|
-
requestAnimationFrame(tick);
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
this.displayAmount(amount);
|
|
868
|
-
this.scale.set(1);
|
|
756
|
+
let elapsed = 0;
|
|
757
|
+
const onTick = (ticker) => {
|
|
758
|
+
elapsed += ticker.deltaMS;
|
|
759
|
+
if (elapsed >= ms) {
|
|
760
|
+
Ticker.shared.remove(onTick);
|
|
869
761
|
resolve();
|
|
870
762
|
}
|
|
871
763
|
};
|
|
872
|
-
|
|
764
|
+
Ticker.shared.add(onTick);
|
|
873
765
|
});
|
|
874
766
|
}
|
|
875
767
|
/**
|
|
876
|
-
*
|
|
768
|
+
* Kill all tweens on a target.
|
|
877
769
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
});
|
|
882
778
|
}
|
|
883
779
|
/**
|
|
884
|
-
*
|
|
780
|
+
* Kill all active tweens.
|
|
885
781
|
*/
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
782
|
+
static killAll() {
|
|
783
|
+
for (const tw of Tween._tweens) {
|
|
784
|
+
tw.resolve();
|
|
785
|
+
}
|
|
786
|
+
Tween._tweens.length = 0;
|
|
889
787
|
}
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
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
|
+
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
|
+
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;
|
|
892
868
|
}
|
|
893
869
|
}
|
|
894
870
|
|
|
@@ -896,6 +872,8 @@ class WinDisplay extends Container {
|
|
|
896
872
|
* Modal overlay component.
|
|
897
873
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
898
874
|
*
|
|
875
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
876
|
+
*
|
|
899
877
|
* @example
|
|
900
878
|
* ```ts
|
|
901
879
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -914,11 +892,10 @@ class Modal extends Container {
|
|
|
914
892
|
constructor(config = {}) {
|
|
915
893
|
super();
|
|
916
894
|
this._config = {
|
|
917
|
-
overlayColor: 0x000000,
|
|
918
|
-
overlayAlpha: 0.7,
|
|
919
|
-
closeOnOverlay: true,
|
|
920
|
-
animationDuration: 300,
|
|
921
|
-
...config,
|
|
895
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
896
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
897
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
898
|
+
animationDuration: config.animationDuration ?? 300,
|
|
922
899
|
};
|
|
923
900
|
// Overlay
|
|
924
901
|
this._overlay = new Graphics();
|
|
@@ -986,6 +963,8 @@ const TOAST_COLORS = {
|
|
|
986
963
|
/**
|
|
987
964
|
* Toast notification component for displaying transient messages.
|
|
988
965
|
*
|
|
966
|
+
* Uses `@pixi/layout` LayoutContainer for auto-sized background.
|
|
967
|
+
*
|
|
989
968
|
* @example
|
|
990
969
|
* ```ts
|
|
991
970
|
* const toast = new Toast();
|
|
@@ -1001,11 +980,10 @@ class Toast extends Container {
|
|
|
1001
980
|
constructor(config = {}) {
|
|
1002
981
|
super();
|
|
1003
982
|
this._config = {
|
|
1004
|
-
duration: 3000,
|
|
1005
|
-
bottomOffset: 60,
|
|
1006
|
-
...config,
|
|
983
|
+
duration: config.duration ?? 3000,
|
|
984
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
1007
985
|
};
|
|
1008
|
-
this._bg = new
|
|
986
|
+
this._bg = new LayoutContainer();
|
|
1009
987
|
this.addChild(this._bg);
|
|
1010
988
|
this._text = new Text({
|
|
1011
989
|
text: '',
|
|
@@ -1023,7 +1001,6 @@ class Toast extends Container {
|
|
|
1023
1001
|
* Show a toast message.
|
|
1024
1002
|
*/
|
|
1025
1003
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
1026
|
-
// Clear previous dismiss
|
|
1027
1004
|
if (this._dismissTimeout) {
|
|
1028
1005
|
clearTimeout(this._dismissTimeout);
|
|
1029
1006
|
}
|
|
@@ -1032,10 +1009,16 @@ class Toast extends Container {
|
|
|
1032
1009
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
1033
1010
|
const height = 44;
|
|
1034
1011
|
const radius = 8;
|
|
1035
|
-
|
|
1036
|
-
this._bg.
|
|
1037
|
-
|
|
1038
|
-
|
|
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;
|
|
1039
1022
|
// Position
|
|
1040
1023
|
if (viewWidth && viewHeight) {
|
|
1041
1024
|
this.x = viewWidth / 2;
|
|
@@ -1044,9 +1027,7 @@ class Toast extends Container {
|
|
|
1044
1027
|
this.visible = true;
|
|
1045
1028
|
this.alpha = 0;
|
|
1046
1029
|
this.y += 20;
|
|
1047
|
-
// Animate in
|
|
1048
1030
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
1049
|
-
// Auto-dismiss
|
|
1050
1031
|
if (this._config.duration > 0) {
|
|
1051
1032
|
this._dismissTimeout = setTimeout(() => {
|
|
1052
1033
|
this.dismiss();
|
|
@@ -1068,5 +1049,291 @@ class Toast extends Container {
|
|
|
1068
1049
|
}
|
|
1069
1050
|
}
|
|
1070
1051
|
|
|
1071
|
-
|
|
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 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 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
|
+
export { BalanceDisplay, Button, Label, Layout, Modal, Panel, ProgressBar, ScrollContainer, Toast, WinDisplay };
|
|
1072
1339
|
//# sourceMappingURL=ui.esm.js.map
|