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