@energy8platform/game-engine 0.1.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 +1134 -0
- package/dist/animation.cjs.js +505 -0
- package/dist/animation.cjs.js.map +1 -0
- package/dist/animation.d.ts +235 -0
- package/dist/animation.esm.js +500 -0
- package/dist/animation.esm.js.map +1 -0
- package/dist/assets.cjs.js +148 -0
- package/dist/assets.cjs.js.map +1 -0
- package/dist/assets.d.ts +97 -0
- package/dist/assets.esm.js +146 -0
- package/dist/assets.esm.js.map +1 -0
- package/dist/audio.cjs.js +345 -0
- package/dist/audio.cjs.js.map +1 -0
- package/dist/audio.d.ts +135 -0
- package/dist/audio.esm.js +343 -0
- package/dist/audio.esm.js.map +1 -0
- package/dist/core.cjs.js +1832 -0
- package/dist/core.cjs.js.map +1 -0
- package/dist/core.d.ts +633 -0
- package/dist/core.esm.js +1827 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/debug.cjs.js +298 -0
- package/dist/debug.cjs.js.map +1 -0
- package/dist/debug.d.ts +114 -0
- package/dist/debug.esm.js +295 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.cjs.js +3623 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +1607 -0
- package/dist/index.esm.js +3598 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/ui.cjs.js +1081 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.d.ts +387 -0
- package/dist/ui.esm.js +1072 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vite.cjs.js +125 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.esm.js +122 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +107 -0
- package/src/animation/Easing.ts +116 -0
- package/src/animation/SpineHelper.ts +162 -0
- package/src/animation/Timeline.ts +138 -0
- package/src/animation/Tween.ts +225 -0
- package/src/animation/index.ts +4 -0
- package/src/assets/AssetManager.ts +174 -0
- package/src/assets/index.ts +2 -0
- package/src/audio/AudioManager.ts +366 -0
- package/src/audio/index.ts +1 -0
- package/src/core/EventEmitter.ts +47 -0
- package/src/core/GameApplication.ts +306 -0
- package/src/core/Scene.ts +48 -0
- package/src/core/SceneManager.ts +258 -0
- package/src/core/index.ts +4 -0
- package/src/debug/DevBridge.ts +259 -0
- package/src/debug/FPSOverlay.ts +102 -0
- package/src/debug/index.ts +3 -0
- package/src/index.ts +71 -0
- package/src/input/InputManager.ts +171 -0
- package/src/input/index.ts +1 -0
- package/src/loading/CSSPreloader.ts +155 -0
- package/src/loading/LoadingScene.ts +356 -0
- package/src/loading/index.ts +2 -0
- package/src/state/StateMachine.ts +228 -0
- package/src/state/index.ts +1 -0
- package/src/types.ts +218 -0
- package/src/ui/BalanceDisplay.ts +155 -0
- package/src/ui/Button.ts +199 -0
- package/src/ui/Label.ts +111 -0
- package/src/ui/Modal.ts +134 -0
- package/src/ui/Panel.ts +125 -0
- package/src/ui/ProgressBar.ts +121 -0
- package/src/ui/Toast.ts +124 -0
- package/src/ui/WinDisplay.ts +133 -0
- package/src/ui/index.ts +16 -0
- package/src/viewport/ViewportManager.ts +241 -0
- package/src/viewport/index.ts +1 -0
- package/src/vite/index.ts +153 -0
package/dist/ui.esm.js
ADDED
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
import { Ticker, Container, Graphics, Texture, Sprite, Text, NineSliceSprite } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collection of easing functions for use with Tween and Timeline.
|
|
5
|
+
*
|
|
6
|
+
* All functions take a progress value t (0..1) and return the eased value.
|
|
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
|
+
}
|
|
250
|
+
|
|
251
|
+
const DEFAULT_COLORS = {
|
|
252
|
+
normal: 0xffd700,
|
|
253
|
+
hover: 0xffe44d,
|
|
254
|
+
pressed: 0xccac00,
|
|
255
|
+
disabled: 0x666666,
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Interactive button component with state management and animation.
|
|
259
|
+
*
|
|
260
|
+
* Supports both texture-based and Graphics-based rendering.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* const btn = new Button({
|
|
265
|
+
* width: 200, height: 60, borderRadius: 12,
|
|
266
|
+
* colors: { normal: 0x22aa22, hover: 0x33cc33 },
|
|
267
|
+
* });
|
|
268
|
+
*
|
|
269
|
+
* btn.onTap = () => console.log('Clicked!');
|
|
270
|
+
* scene.container.addChild(btn);
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
class Button extends Container {
|
|
274
|
+
_state = 'normal';
|
|
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;
|
|
282
|
+
constructor(config = {}) {
|
|
283
|
+
super();
|
|
284
|
+
this._config = {
|
|
285
|
+
width: 200,
|
|
286
|
+
height: 60,
|
|
287
|
+
borderRadius: 8,
|
|
288
|
+
pressScale: 0.95,
|
|
289
|
+
animationDuration: 100,
|
|
290
|
+
...config,
|
|
291
|
+
};
|
|
292
|
+
// Create Graphics background
|
|
293
|
+
this._bg = new Graphics();
|
|
294
|
+
this.addChild(this._bg);
|
|
295
|
+
// Create texture sprites if provided
|
|
296
|
+
if (config.textures) {
|
|
297
|
+
for (const [state, tex] of Object.entries(config.textures)) {
|
|
298
|
+
const texture = typeof tex === 'string' ? Texture.from(tex) : tex;
|
|
299
|
+
const sprite = new Sprite(texture);
|
|
300
|
+
sprite.anchor.set(0.5);
|
|
301
|
+
sprite.visible = state === 'normal';
|
|
302
|
+
this._sprites[state] = sprite;
|
|
303
|
+
this.addChild(sprite);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Make interactive
|
|
307
|
+
this.eventMode = 'static';
|
|
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');
|
|
319
|
+
if (config.disabled) {
|
|
320
|
+
this.disable();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** Current button state */
|
|
324
|
+
get state() {
|
|
325
|
+
return this._state;
|
|
326
|
+
}
|
|
327
|
+
/** Enable the button */
|
|
328
|
+
enable() {
|
|
329
|
+
if (this._state === 'disabled') {
|
|
330
|
+
this.setState('normal');
|
|
331
|
+
this.eventMode = 'static';
|
|
332
|
+
this.cursor = 'pointer';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** Disable the button */
|
|
336
|
+
disable() {
|
|
337
|
+
this.setState('disabled');
|
|
338
|
+
this.eventMode = 'none';
|
|
339
|
+
this.cursor = 'default';
|
|
340
|
+
}
|
|
341
|
+
/** Whether the button is disabled */
|
|
342
|
+
get disabled() {
|
|
343
|
+
return this._state === 'disabled';
|
|
344
|
+
}
|
|
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
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Horizontal progress bar with optional smooth fill animation.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```ts
|
|
410
|
+
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
411
|
+
* scene.container.addChild(bar);
|
|
412
|
+
* bar.progress = 0.5; // 50%
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
class ProgressBar extends Container {
|
|
416
|
+
_track;
|
|
417
|
+
_fill;
|
|
418
|
+
_border;
|
|
419
|
+
_config;
|
|
420
|
+
_progress = 0;
|
|
421
|
+
_displayedProgress = 0;
|
|
422
|
+
constructor(config = {}) {
|
|
423
|
+
super();
|
|
424
|
+
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
|
+
...config,
|
|
435
|
+
};
|
|
436
|
+
this._track = new Graphics();
|
|
437
|
+
this._fill = new Graphics();
|
|
438
|
+
this._border = new Graphics();
|
|
439
|
+
this.addChild(this._track, this._fill, this._border);
|
|
440
|
+
this.drawTrack();
|
|
441
|
+
this.drawBorder();
|
|
442
|
+
this.drawFill(0);
|
|
443
|
+
}
|
|
444
|
+
/** Get/set progress (0..1) */
|
|
445
|
+
get progress() {
|
|
446
|
+
return this._progress;
|
|
447
|
+
}
|
|
448
|
+
set progress(value) {
|
|
449
|
+
this._progress = Math.max(0, Math.min(1, value));
|
|
450
|
+
if (!this._config.animated) {
|
|
451
|
+
this._displayedProgress = this._progress;
|
|
452
|
+
this.drawFill(this._displayedProgress);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Call each frame if animated is true.
|
|
457
|
+
*/
|
|
458
|
+
update(dt) {
|
|
459
|
+
if (!this._config.animated)
|
|
460
|
+
return;
|
|
461
|
+
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
462
|
+
this._displayedProgress = this._progress;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this._displayedProgress +=
|
|
466
|
+
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
467
|
+
this.drawFill(this._displayedProgress);
|
|
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
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Enhanced text label with auto-fit scaling and currency formatting.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```ts
|
|
504
|
+
* const label = new Label({
|
|
505
|
+
* text: 'BALANCE',
|
|
506
|
+
* style: { fontSize: 24, fill: 0xffd700 },
|
|
507
|
+
* maxWidth: 200,
|
|
508
|
+
* autoFit: true,
|
|
509
|
+
* });
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
class Label extends Container {
|
|
513
|
+
_text;
|
|
514
|
+
_maxWidth;
|
|
515
|
+
_autoFit;
|
|
516
|
+
constructor(config = {}) {
|
|
517
|
+
super();
|
|
518
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
519
|
+
this._autoFit = config.autoFit ?? false;
|
|
520
|
+
this._text = new Text({
|
|
521
|
+
text: config.text ?? '',
|
|
522
|
+
style: {
|
|
523
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
524
|
+
fontSize: 24,
|
|
525
|
+
fill: 0xffffff,
|
|
526
|
+
...config.style,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
this._text.anchor.set(0.5);
|
|
530
|
+
this.addChild(this._text);
|
|
531
|
+
this.fitText();
|
|
532
|
+
}
|
|
533
|
+
/** Get/set the displayed text */
|
|
534
|
+
get text() {
|
|
535
|
+
return this._text.text;
|
|
536
|
+
}
|
|
537
|
+
set text(value) {
|
|
538
|
+
this._text.text = value;
|
|
539
|
+
this.fitText();
|
|
540
|
+
}
|
|
541
|
+
/** Get/set the text style */
|
|
542
|
+
get style() {
|
|
543
|
+
return this._text.style;
|
|
544
|
+
}
|
|
545
|
+
/** Set max width constraint */
|
|
546
|
+
set maxWidth(value) {
|
|
547
|
+
this._maxWidth = value;
|
|
548
|
+
this.fitText();
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Format and display a number as currency.
|
|
552
|
+
*
|
|
553
|
+
* @param amount - The numeric amount
|
|
554
|
+
* @param currency - Currency code (e.g., 'USD', 'EUR')
|
|
555
|
+
* @param locale - Locale string (default: 'en-US')
|
|
556
|
+
*/
|
|
557
|
+
setCurrency(amount, currency, locale = 'en-US') {
|
|
558
|
+
try {
|
|
559
|
+
this.text = new Intl.NumberFormat(locale, {
|
|
560
|
+
style: 'currency',
|
|
561
|
+
currency,
|
|
562
|
+
minimumFractionDigits: 2,
|
|
563
|
+
maximumFractionDigits: 2,
|
|
564
|
+
}).format(amount);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
this.text = `${amount.toFixed(2)} ${currency}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Format a number with thousands separators.
|
|
572
|
+
*/
|
|
573
|
+
setNumber(value, decimals = 0, locale = 'en-US') {
|
|
574
|
+
this.text = new Intl.NumberFormat(locale, {
|
|
575
|
+
minimumFractionDigits: decimals,
|
|
576
|
+
maximumFractionDigits: decimals,
|
|
577
|
+
}).format(value);
|
|
578
|
+
}
|
|
579
|
+
fitText() {
|
|
580
|
+
if (!this._autoFit || this._maxWidth === Infinity)
|
|
581
|
+
return;
|
|
582
|
+
this._text.scale.set(1);
|
|
583
|
+
if (this._text.width > this._maxWidth) {
|
|
584
|
+
const scale = this._maxWidth / this._text.width;
|
|
585
|
+
this._text.scale.set(scale);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Background panel that can use either Graphics or 9-slice sprite.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```ts
|
|
595
|
+
* // Simple colored panel
|
|
596
|
+
* const panel = new Panel({ width: 400, height: 300, backgroundColor: 0x222222, borderRadius: 12 });
|
|
597
|
+
*
|
|
598
|
+
* // 9-slice panel (texture-based)
|
|
599
|
+
* const panel = new Panel({
|
|
600
|
+
* nineSliceTexture: 'panel-bg',
|
|
601
|
+
* nineSliceBorders: [20, 20, 20, 20],
|
|
602
|
+
* width: 400, height: 300,
|
|
603
|
+
* });
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
class Panel extends Container {
|
|
607
|
+
_bg;
|
|
608
|
+
_content;
|
|
609
|
+
_config;
|
|
610
|
+
constructor(config = {}) {
|
|
611
|
+
super();
|
|
612
|
+
this._config = {
|
|
613
|
+
width: 400,
|
|
614
|
+
height: 300,
|
|
615
|
+
padding: 16,
|
|
616
|
+
backgroundAlpha: 1,
|
|
617
|
+
...config,
|
|
618
|
+
};
|
|
619
|
+
// Create background
|
|
620
|
+
if (config.nineSliceTexture) {
|
|
621
|
+
const texture = typeof config.nineSliceTexture === 'string'
|
|
622
|
+
? Texture.from(config.nineSliceTexture)
|
|
623
|
+
: config.nineSliceTexture;
|
|
624
|
+
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
625
|
+
this._bg = new NineSliceSprite({
|
|
626
|
+
texture,
|
|
627
|
+
leftWidth: left,
|
|
628
|
+
topHeight: top,
|
|
629
|
+
rightWidth: right,
|
|
630
|
+
bottomHeight: bottom,
|
|
631
|
+
});
|
|
632
|
+
this._bg.width = this._config.width;
|
|
633
|
+
this._bg.height = this._config.height;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
this._bg = new Graphics();
|
|
637
|
+
this.drawGraphicsBg();
|
|
638
|
+
}
|
|
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
|
+
}
|
|
647
|
+
/** Content container — add children here */
|
|
648
|
+
get content() {
|
|
649
|
+
return this._content;
|
|
650
|
+
}
|
|
651
|
+
/** Resize the panel */
|
|
652
|
+
setSize(width, height) {
|
|
653
|
+
this._config.width = width;
|
|
654
|
+
this._config.height = height;
|
|
655
|
+
if (this._bg instanceof Graphics) {
|
|
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
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Reactive balance display component.
|
|
677
|
+
*
|
|
678
|
+
* Automatically formats currency and can animate value changes
|
|
679
|
+
* with a smooth countup/countdown effect.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```ts
|
|
683
|
+
* const balance = new BalanceDisplay({ currency: 'USD', animated: true });
|
|
684
|
+
* balance.setValue(1000);
|
|
685
|
+
*
|
|
686
|
+
* // Wire to SDK
|
|
687
|
+
* sdk.on('balanceUpdate', ({ balance: val }) => balance.setValue(val));
|
|
688
|
+
* ```
|
|
689
|
+
*/
|
|
690
|
+
class BalanceDisplay extends Container {
|
|
691
|
+
_prefixLabel = null;
|
|
692
|
+
_valueLabel;
|
|
693
|
+
_config;
|
|
694
|
+
_currentValue = 0;
|
|
695
|
+
_displayedValue = 0;
|
|
696
|
+
_animating = false;
|
|
697
|
+
constructor(config = {}) {
|
|
698
|
+
super();
|
|
699
|
+
this._config = {
|
|
700
|
+
currency: config.currency ?? 'USD',
|
|
701
|
+
locale: config.locale ?? 'en-US',
|
|
702
|
+
animated: config.animated ?? true,
|
|
703
|
+
animationDuration: config.animationDuration ?? 500,
|
|
704
|
+
};
|
|
705
|
+
// Prefix label
|
|
706
|
+
if (config.prefix) {
|
|
707
|
+
this._prefixLabel = new Label({
|
|
708
|
+
text: config.prefix,
|
|
709
|
+
style: {
|
|
710
|
+
fontSize: 16,
|
|
711
|
+
fill: 0xaaaaaa,
|
|
712
|
+
...config.style,
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
this.addChild(this._prefixLabel);
|
|
716
|
+
}
|
|
717
|
+
// Value label
|
|
718
|
+
this._valueLabel = new Label({
|
|
719
|
+
text: '0.00',
|
|
720
|
+
style: {
|
|
721
|
+
fontSize: 28,
|
|
722
|
+
fontWeight: 'bold',
|
|
723
|
+
fill: 0xffffff,
|
|
724
|
+
...config.style,
|
|
725
|
+
},
|
|
726
|
+
maxWidth: config.maxWidth,
|
|
727
|
+
autoFit: !!config.maxWidth,
|
|
728
|
+
});
|
|
729
|
+
this.addChild(this._valueLabel);
|
|
730
|
+
this.layoutLabels();
|
|
731
|
+
}
|
|
732
|
+
/** Current displayed value */
|
|
733
|
+
get value() {
|
|
734
|
+
return this._currentValue;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Set the balance value. If animated, smoothly counts to the new value.
|
|
738
|
+
*/
|
|
739
|
+
setValue(value) {
|
|
740
|
+
const oldValue = this._currentValue;
|
|
741
|
+
this._currentValue = value;
|
|
742
|
+
if (this._config.animated && oldValue !== value) {
|
|
743
|
+
this.animateValue(oldValue, value);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
this._displayedValue = value;
|
|
747
|
+
this.updateDisplay();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Set the currency code.
|
|
752
|
+
*/
|
|
753
|
+
setCurrency(currency) {
|
|
754
|
+
this._config.currency = currency;
|
|
755
|
+
this.updateDisplay();
|
|
756
|
+
}
|
|
757
|
+
async animateValue(from, to) {
|
|
758
|
+
this._animating = true;
|
|
759
|
+
const duration = this._config.animationDuration;
|
|
760
|
+
const startTime = Date.now();
|
|
761
|
+
return new Promise((resolve) => {
|
|
762
|
+
const tick = () => {
|
|
763
|
+
const elapsed = Date.now() - startTime;
|
|
764
|
+
const t = Math.min(elapsed / duration, 1);
|
|
765
|
+
const eased = Easing.easeOutCubic(t);
|
|
766
|
+
this._displayedValue = from + (to - from) * eased;
|
|
767
|
+
this.updateDisplay();
|
|
768
|
+
if (t < 1) {
|
|
769
|
+
requestAnimationFrame(tick);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
this._displayedValue = to;
|
|
773
|
+
this.updateDisplay();
|
|
774
|
+
this._animating = false;
|
|
775
|
+
resolve();
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
requestAnimationFrame(tick);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
updateDisplay() {
|
|
782
|
+
this._valueLabel.setCurrency(this._displayedValue, this._config.currency, this._config.locale);
|
|
783
|
+
}
|
|
784
|
+
layoutLabels() {
|
|
785
|
+
if (this._prefixLabel) {
|
|
786
|
+
this._prefixLabel.y = -14;
|
|
787
|
+
this._valueLabel.y = 14;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Win amount display with countup animation.
|
|
794
|
+
*
|
|
795
|
+
* Shows a dramatic countup from 0 to the win amount, with optional
|
|
796
|
+
* scale pop effect — typical of slot games.
|
|
797
|
+
*
|
|
798
|
+
* @example
|
|
799
|
+
* ```ts
|
|
800
|
+
* const winDisplay = new WinDisplay({ currency: 'USD' });
|
|
801
|
+
* scene.container.addChild(winDisplay);
|
|
802
|
+
* await winDisplay.showWin(150.50); // countup animation
|
|
803
|
+
* winDisplay.hide();
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
806
|
+
class WinDisplay extends Container {
|
|
807
|
+
_label;
|
|
808
|
+
_config;
|
|
809
|
+
_cancelCountup = false;
|
|
810
|
+
constructor(config = {}) {
|
|
811
|
+
super();
|
|
812
|
+
this._config = {
|
|
813
|
+
currency: config.currency ?? 'USD',
|
|
814
|
+
locale: config.locale ?? 'en-US',
|
|
815
|
+
countupDuration: config.countupDuration ?? 1500,
|
|
816
|
+
popScale: config.popScale ?? 1.2,
|
|
817
|
+
};
|
|
818
|
+
this._label = new Label({
|
|
819
|
+
text: '',
|
|
820
|
+
style: {
|
|
821
|
+
fontSize: 48,
|
|
822
|
+
fontWeight: 'bold',
|
|
823
|
+
fill: 0xffd700,
|
|
824
|
+
stroke: { color: 0x000000, width: 3 },
|
|
825
|
+
...config.style,
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
this.addChild(this._label);
|
|
829
|
+
this.visible = false;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Show a win with countup animation.
|
|
833
|
+
*
|
|
834
|
+
* @param amount - Win amount
|
|
835
|
+
* @returns Promise that resolves when the animation completes
|
|
836
|
+
*/
|
|
837
|
+
async showWin(amount) {
|
|
838
|
+
this.visible = true;
|
|
839
|
+
this._cancelCountup = false;
|
|
840
|
+
this.alpha = 1;
|
|
841
|
+
const duration = this._config.countupDuration;
|
|
842
|
+
const startTime = Date.now();
|
|
843
|
+
// Scale pop
|
|
844
|
+
this.scale.set(0.5);
|
|
845
|
+
return new Promise((resolve) => {
|
|
846
|
+
const tick = () => {
|
|
847
|
+
if (this._cancelCountup) {
|
|
848
|
+
this.displayAmount(amount);
|
|
849
|
+
resolve();
|
|
850
|
+
return;
|
|
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);
|
|
869
|
+
resolve();
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
requestAnimationFrame(tick);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Skip the countup animation and show the final amount immediately.
|
|
877
|
+
*/
|
|
878
|
+
skipCountup(amount) {
|
|
879
|
+
this._cancelCountup = true;
|
|
880
|
+
this.displayAmount(amount);
|
|
881
|
+
this.scale.set(1);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Hide the win display.
|
|
885
|
+
*/
|
|
886
|
+
hide() {
|
|
887
|
+
this.visible = false;
|
|
888
|
+
this._label.text = '';
|
|
889
|
+
}
|
|
890
|
+
displayAmount(amount) {
|
|
891
|
+
this._label.setCurrency(amount, this._config.currency, this._config.locale);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Modal overlay component.
|
|
897
|
+
* Shows content on top of a dark overlay with enter/exit animations.
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* ```ts
|
|
901
|
+
* const modal = new Modal({ closeOnOverlay: true });
|
|
902
|
+
* modal.content.addChild(settingsPanel);
|
|
903
|
+
* modal.onClose = () => console.log('Closed');
|
|
904
|
+
* await modal.show(1920, 1080);
|
|
905
|
+
* ```
|
|
906
|
+
*/
|
|
907
|
+
class Modal extends Container {
|
|
908
|
+
_overlay;
|
|
909
|
+
_contentContainer;
|
|
910
|
+
_config;
|
|
911
|
+
_showing = false;
|
|
912
|
+
/** Called when the modal is closed */
|
|
913
|
+
onClose;
|
|
914
|
+
constructor(config = {}) {
|
|
915
|
+
super();
|
|
916
|
+
this._config = {
|
|
917
|
+
overlayColor: 0x000000,
|
|
918
|
+
overlayAlpha: 0.7,
|
|
919
|
+
closeOnOverlay: true,
|
|
920
|
+
animationDuration: 300,
|
|
921
|
+
...config,
|
|
922
|
+
};
|
|
923
|
+
// Overlay
|
|
924
|
+
this._overlay = new Graphics();
|
|
925
|
+
this._overlay.eventMode = 'static';
|
|
926
|
+
this.addChild(this._overlay);
|
|
927
|
+
if (this._config.closeOnOverlay) {
|
|
928
|
+
this._overlay.on('pointertap', () => this.hide());
|
|
929
|
+
}
|
|
930
|
+
// Content container
|
|
931
|
+
this._contentContainer = new Container();
|
|
932
|
+
this.addChild(this._contentContainer);
|
|
933
|
+
this.visible = false;
|
|
934
|
+
}
|
|
935
|
+
/** Content container — add your UI here */
|
|
936
|
+
get content() {
|
|
937
|
+
return this._contentContainer;
|
|
938
|
+
}
|
|
939
|
+
/** Whether the modal is currently showing */
|
|
940
|
+
get isShowing() {
|
|
941
|
+
return this._showing;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Show the modal with animation.
|
|
945
|
+
*/
|
|
946
|
+
async show(viewWidth, viewHeight) {
|
|
947
|
+
this._showing = true;
|
|
948
|
+
this.visible = true;
|
|
949
|
+
// Draw overlay to cover full screen
|
|
950
|
+
this._overlay.clear();
|
|
951
|
+
this._overlay.rect(0, 0, viewWidth, viewHeight).fill(this._config.overlayColor);
|
|
952
|
+
this._overlay.alpha = 0;
|
|
953
|
+
// Center content
|
|
954
|
+
this._contentContainer.x = viewWidth / 2;
|
|
955
|
+
this._contentContainer.y = viewHeight / 2;
|
|
956
|
+
this._contentContainer.alpha = 0;
|
|
957
|
+
this._contentContainer.scale.set(0.8);
|
|
958
|
+
// Animate in
|
|
959
|
+
await Promise.all([
|
|
960
|
+
Tween.to(this._overlay, { alpha: this._config.overlayAlpha }, this._config.animationDuration, Easing.easeOutCubic),
|
|
961
|
+
Tween.to(this._contentContainer, { alpha: 1, 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutBack),
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Hide the modal with animation.
|
|
966
|
+
*/
|
|
967
|
+
async hide() {
|
|
968
|
+
if (!this._showing)
|
|
969
|
+
return;
|
|
970
|
+
await Promise.all([
|
|
971
|
+
Tween.to(this._overlay, { alpha: 0 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
|
|
972
|
+
Tween.to(this._contentContainer, { alpha: 0, 'scale.x': 0.8, 'scale.y': 0.8 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
|
|
973
|
+
]);
|
|
974
|
+
this.visible = false;
|
|
975
|
+
this._showing = false;
|
|
976
|
+
this.onClose?.();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const TOAST_COLORS = {
|
|
981
|
+
info: 0x3498db,
|
|
982
|
+
success: 0x27ae60,
|
|
983
|
+
warning: 0xf39c12,
|
|
984
|
+
error: 0xe74c3c,
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* Toast notification component for displaying transient messages.
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* ```ts
|
|
991
|
+
* const toast = new Toast();
|
|
992
|
+
* scene.container.addChild(toast);
|
|
993
|
+
* await toast.show('Connection lost', 'error', 1920, 1080);
|
|
994
|
+
* ```
|
|
995
|
+
*/
|
|
996
|
+
class Toast extends Container {
|
|
997
|
+
_bg;
|
|
998
|
+
_text;
|
|
999
|
+
_config;
|
|
1000
|
+
_dismissTimeout = null;
|
|
1001
|
+
constructor(config = {}) {
|
|
1002
|
+
super();
|
|
1003
|
+
this._config = {
|
|
1004
|
+
duration: 3000,
|
|
1005
|
+
bottomOffset: 60,
|
|
1006
|
+
...config,
|
|
1007
|
+
};
|
|
1008
|
+
this._bg = new Graphics();
|
|
1009
|
+
this.addChild(this._bg);
|
|
1010
|
+
this._text = new Text({
|
|
1011
|
+
text: '',
|
|
1012
|
+
style: {
|
|
1013
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
1014
|
+
fontSize: 16,
|
|
1015
|
+
fill: 0xffffff,
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
this._text.anchor.set(0.5);
|
|
1019
|
+
this.addChild(this._text);
|
|
1020
|
+
this.visible = false;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Show a toast message.
|
|
1024
|
+
*/
|
|
1025
|
+
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
1026
|
+
// Clear previous dismiss
|
|
1027
|
+
if (this._dismissTimeout) {
|
|
1028
|
+
clearTimeout(this._dismissTimeout);
|
|
1029
|
+
}
|
|
1030
|
+
this._text.text = message;
|
|
1031
|
+
const padding = 20;
|
|
1032
|
+
const width = Math.max(200, this._text.width + padding * 2);
|
|
1033
|
+
const height = 44;
|
|
1034
|
+
const radius = 8;
|
|
1035
|
+
this._bg.clear();
|
|
1036
|
+
this._bg.roundRect(-width / 2, -height / 2, width, height, radius).fill(TOAST_COLORS[type]);
|
|
1037
|
+
this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
|
|
1038
|
+
.fill({ color: 0x000000, alpha: 0.2 });
|
|
1039
|
+
// Position
|
|
1040
|
+
if (viewWidth && viewHeight) {
|
|
1041
|
+
this.x = viewWidth / 2;
|
|
1042
|
+
this.y = viewHeight - this._config.bottomOffset;
|
|
1043
|
+
}
|
|
1044
|
+
this.visible = true;
|
|
1045
|
+
this.alpha = 0;
|
|
1046
|
+
this.y += 20;
|
|
1047
|
+
// Animate in
|
|
1048
|
+
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
1049
|
+
// Auto-dismiss
|
|
1050
|
+
if (this._config.duration > 0) {
|
|
1051
|
+
this._dismissTimeout = setTimeout(() => {
|
|
1052
|
+
this.dismiss();
|
|
1053
|
+
}, this._config.duration);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Dismiss the toast.
|
|
1058
|
+
*/
|
|
1059
|
+
async dismiss() {
|
|
1060
|
+
if (!this.visible)
|
|
1061
|
+
return;
|
|
1062
|
+
if (this._dismissTimeout) {
|
|
1063
|
+
clearTimeout(this._dismissTimeout);
|
|
1064
|
+
this._dismissTimeout = null;
|
|
1065
|
+
}
|
|
1066
|
+
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
1067
|
+
this.visible = false;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export { BalanceDisplay, Button, Label, Modal, Panel, ProgressBar, Toast, WinDisplay };
|
|
1072
|
+
//# sourceMappingURL=ui.esm.js.map
|