@energy8platform/game-engine 0.3.0 → 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 +139 -44
- package/dist/core.cjs.js +1 -0
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +1 -0
- package/dist/core.esm.js.map +1 -1
- package/dist/index.cjs.js +317 -789
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +61 -129
- package/dist/index.esm.js +304 -790
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +637 -1106
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +60 -128
- package/dist/ui.esm.js +620 -1107
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +23 -3
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +23 -3
- package/dist/vite.esm.js.map +1 -1
- package/package.json +17 -2
- package/src/core/GameApplication.ts +1 -0
- package/src/index.ts +16 -0
- package/src/ui/BalanceDisplay.ts +0 -3
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +102 -180
- 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 +58 -489
- package/src/ui/Toast.ts +19 -13
- package/src/ui/index.ts +13 -0
- package/src/vite/index.ts +23 -3
package/dist/ui.cjs.js
CHANGED
|
@@ -1,436 +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
|
-
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
160
|
-
*/
|
|
161
|
-
static delay(ms) {
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
let elapsed = 0;
|
|
164
|
-
const onTick = (ticker) => {
|
|
165
|
-
elapsed += ticker.deltaMS;
|
|
166
|
-
if (elapsed >= ms) {
|
|
167
|
-
pixi_js.Ticker.shared.remove(onTick);
|
|
168
|
-
resolve();
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
pixi_js.Ticker.shared.add(onTick);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Kill all tweens on a target.
|
|
176
|
-
*/
|
|
177
|
-
static killTweensOf(target) {
|
|
178
|
-
Tween._tweens = Tween._tweens.filter((tw) => {
|
|
179
|
-
if (tw.target === target) {
|
|
180
|
-
tw.resolve();
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
return true;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Kill all active tweens.
|
|
188
|
-
*/
|
|
189
|
-
static killAll() {
|
|
190
|
-
for (const tw of Tween._tweens) {
|
|
191
|
-
tw.resolve();
|
|
192
|
-
}
|
|
193
|
-
Tween._tweens.length = 0;
|
|
194
|
-
}
|
|
195
|
-
/** Number of active tweens */
|
|
196
|
-
static get activeTweens() {
|
|
197
|
-
return Tween._tweens.length;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Reset the tween system — kill all tweens and remove the ticker.
|
|
201
|
-
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
202
|
-
*/
|
|
203
|
-
static reset() {
|
|
204
|
-
for (const tw of Tween._tweens) {
|
|
205
|
-
tw.resolve();
|
|
206
|
-
}
|
|
207
|
-
Tween._tweens.length = 0;
|
|
208
|
-
if (Tween._tickerAdded) {
|
|
209
|
-
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
210
|
-
Tween._tickerAdded = false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// ─── Internal ──────────────────────────────────────────
|
|
214
|
-
static ensureTicker() {
|
|
215
|
-
if (Tween._tickerAdded)
|
|
216
|
-
return;
|
|
217
|
-
Tween._tickerAdded = true;
|
|
218
|
-
pixi_js.Ticker.shared.add(Tween.tick);
|
|
219
|
-
}
|
|
220
|
-
static tick = (ticker) => {
|
|
221
|
-
const dt = ticker.deltaMS;
|
|
222
|
-
const completed = [];
|
|
223
|
-
for (const tw of Tween._tweens) {
|
|
224
|
-
tw.elapsed += dt;
|
|
225
|
-
if (tw.elapsed < tw.delay)
|
|
226
|
-
continue;
|
|
227
|
-
const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
|
|
228
|
-
const t = tw.easing(raw);
|
|
229
|
-
// Interpolate each property
|
|
230
|
-
for (const key of Object.keys(tw.to)) {
|
|
231
|
-
const start = tw.from[key];
|
|
232
|
-
const end = tw.to[key];
|
|
233
|
-
const value = start + (end - start) * t;
|
|
234
|
-
Tween.setProperty(tw.target, key, value);
|
|
235
|
-
}
|
|
236
|
-
tw.onUpdate?.(raw);
|
|
237
|
-
if (raw >= 1) {
|
|
238
|
-
completed.push(tw);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Remove completed tweens
|
|
242
|
-
for (const tw of completed) {
|
|
243
|
-
const idx = Tween._tweens.indexOf(tw);
|
|
244
|
-
if (idx !== -1)
|
|
245
|
-
Tween._tweens.splice(idx, 1);
|
|
246
|
-
tw.resolve();
|
|
247
|
-
}
|
|
248
|
-
// Remove ticker when no active tweens
|
|
249
|
-
if (Tween._tweens.length === 0 && Tween._tickerAdded) {
|
|
250
|
-
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
251
|
-
Tween._tickerAdded = false;
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
/**
|
|
255
|
-
* Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
|
|
256
|
-
*/
|
|
257
|
-
static getProperty(target, key) {
|
|
258
|
-
const parts = key.split('.');
|
|
259
|
-
let obj = target;
|
|
260
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
261
|
-
obj = obj[parts[i]];
|
|
262
|
-
}
|
|
263
|
-
return obj[parts[parts.length - 1]] ?? 0;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Set a potentially nested property.
|
|
267
|
-
*/
|
|
268
|
-
static setProperty(target, key, value) {
|
|
269
|
-
const parts = key.split('.');
|
|
270
|
-
let obj = target;
|
|
271
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
272
|
-
obj = obj[parts[i]];
|
|
273
|
-
}
|
|
274
|
-
obj[parts[parts.length - 1]] = value;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
5
|
+
var ui = require('@pixi/ui');
|
|
6
|
+
var components = require('@pixi/layout/components');
|
|
277
7
|
|
|
278
8
|
const DEFAULT_COLORS = {
|
|
279
|
-
|
|
9
|
+
default: 0xffd700,
|
|
280
10
|
hover: 0xffe44d,
|
|
281
11
|
pressed: 0xccac00,
|
|
282
12
|
disabled: 0x666666,
|
|
283
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
|
+
}
|
|
284
21
|
/**
|
|
285
|
-
* Interactive button component
|
|
22
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
286
23
|
*
|
|
287
|
-
* 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.
|
|
288
26
|
*
|
|
289
27
|
* @example
|
|
290
28
|
* ```ts
|
|
291
29
|
* const btn = new Button({
|
|
292
30
|
* width: 200, height: 60, borderRadius: 12,
|
|
293
|
-
* colors: {
|
|
31
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
32
|
+
* text: 'SPIN',
|
|
294
33
|
* });
|
|
295
34
|
*
|
|
296
|
-
* btn.
|
|
35
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
297
36
|
* scene.container.addChild(btn);
|
|
298
37
|
* ```
|
|
299
38
|
*/
|
|
300
|
-
class Button extends
|
|
301
|
-
|
|
302
|
-
_bg;
|
|
303
|
-
_sprites = {};
|
|
304
|
-
_config;
|
|
305
|
-
/** Called when the button is tapped/clicked */
|
|
306
|
-
onTap;
|
|
307
|
-
/** Called when the button state changes */
|
|
308
|
-
onStateChange;
|
|
39
|
+
class Button extends ui.FancyButton {
|
|
40
|
+
_buttonConfig;
|
|
309
41
|
constructor(config = {}) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
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,
|
|
317
48
|
...config,
|
|
318
49
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
323
67
|
if (config.textures) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
332
76
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.
|
|
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;
|
|
87
|
+
}
|
|
88
|
+
super(options);
|
|
89
|
+
this._buttonConfig = resolvedConfig;
|
|
346
90
|
if (config.disabled) {
|
|
347
|
-
this.
|
|
91
|
+
this.enabled = false;
|
|
348
92
|
}
|
|
349
93
|
}
|
|
350
|
-
/** Current button state */
|
|
351
|
-
get state() {
|
|
352
|
-
return this._state;
|
|
353
|
-
}
|
|
354
94
|
/** Enable the button */
|
|
355
95
|
enable() {
|
|
356
|
-
|
|
357
|
-
this.setState('normal');
|
|
358
|
-
this.eventMode = 'static';
|
|
359
|
-
this.cursor = 'pointer';
|
|
360
|
-
}
|
|
96
|
+
this.enabled = true;
|
|
361
97
|
}
|
|
362
98
|
/** Disable the button */
|
|
363
99
|
disable() {
|
|
364
|
-
this.
|
|
365
|
-
this.eventMode = 'none';
|
|
366
|
-
this.cursor = 'default';
|
|
100
|
+
this.enabled = false;
|
|
367
101
|
}
|
|
368
102
|
/** Whether the button is disabled */
|
|
369
103
|
get disabled() {
|
|
370
|
-
return this.
|
|
371
|
-
}
|
|
372
|
-
setState(state) {
|
|
373
|
-
if (this._state === state)
|
|
374
|
-
return;
|
|
375
|
-
this._state = state;
|
|
376
|
-
this.render();
|
|
377
|
-
this.onStateChange?.(state);
|
|
378
|
-
}
|
|
379
|
-
render() {
|
|
380
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
381
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
382
|
-
// Update Graphics
|
|
383
|
-
this._bg.clear();
|
|
384
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
385
|
-
// Add highlight for normal/hover
|
|
386
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
387
|
-
this._bg
|
|
388
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
389
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
390
|
-
}
|
|
391
|
-
// Update sprite visibility
|
|
392
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
393
|
-
if (sprite)
|
|
394
|
-
sprite.visible = state === this._state;
|
|
395
|
-
}
|
|
396
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
397
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
398
|
-
this._sprites.normal.visible = true;
|
|
399
|
-
}
|
|
104
|
+
return !this.enabled;
|
|
400
105
|
}
|
|
401
|
-
onPointerOver = () => {
|
|
402
|
-
if (this._state === 'disabled')
|
|
403
|
-
return;
|
|
404
|
-
this.setState('hover');
|
|
405
|
-
};
|
|
406
|
-
onPointerOut = () => {
|
|
407
|
-
if (this._state === 'disabled')
|
|
408
|
-
return;
|
|
409
|
-
this.setState('normal');
|
|
410
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
411
|
-
};
|
|
412
|
-
onPointerDown = () => {
|
|
413
|
-
if (this._state === 'disabled')
|
|
414
|
-
return;
|
|
415
|
-
this.setState('pressed');
|
|
416
|
-
const s = this._config.pressScale;
|
|
417
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
418
|
-
};
|
|
419
|
-
onPointerUp = () => {
|
|
420
|
-
if (this._state === 'disabled')
|
|
421
|
-
return;
|
|
422
|
-
this.setState('hover');
|
|
423
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
424
|
-
};
|
|
425
|
-
onPointerTap = () => {
|
|
426
|
-
if (this._state === 'disabled')
|
|
427
|
-
return;
|
|
428
|
-
this.onTap?.();
|
|
429
|
-
};
|
|
430
106
|
}
|
|
431
107
|
|
|
108
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
109
|
+
return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
110
|
+
}
|
|
432
111
|
/**
|
|
433
|
-
* Horizontal progress bar
|
|
112
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
113
|
+
*
|
|
114
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
434
115
|
*
|
|
435
116
|
* @example
|
|
436
117
|
* ```ts
|
|
@@ -440,33 +121,48 @@ class Button extends pixi_js.Container {
|
|
|
440
121
|
* ```
|
|
441
122
|
*/
|
|
442
123
|
class ProgressBar extends pixi_js.Container {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
_border;
|
|
124
|
+
_bar;
|
|
125
|
+
_borderGfx;
|
|
446
126
|
_config;
|
|
447
127
|
_progress = 0;
|
|
448
128
|
_displayedProgress = 0;
|
|
449
129
|
constructor(config = {}) {
|
|
450
130
|
super();
|
|
451
131
|
this._config = {
|
|
452
|
-
width: 300,
|
|
453
|
-
height: 16,
|
|
454
|
-
borderRadius: 8,
|
|
455
|
-
fillColor: 0xffd700,
|
|
456
|
-
trackColor: 0x333333,
|
|
457
|
-
borderColor: 0x555555,
|
|
458
|
-
borderWidth: 1,
|
|
459
|
-
animated: true,
|
|
460
|
-
animationSpeed: 0.1,
|
|
461
|
-
...config,
|
|
132
|
+
width: config.width ?? 300,
|
|
133
|
+
height: config.height ?? 16,
|
|
134
|
+
borderRadius: config.borderRadius ?? 8,
|
|
135
|
+
fillColor: config.fillColor ?? 0xffd700,
|
|
136
|
+
trackColor: config.trackColor ?? 0x333333,
|
|
137
|
+
borderColor: config.borderColor ?? 0x555555,
|
|
138
|
+
borderWidth: config.borderWidth ?? 1,
|
|
139
|
+
animated: config.animated ?? true,
|
|
140
|
+
animationSpeed: config.animationSpeed ?? 0.1,
|
|
462
141
|
};
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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,
|
|
155
|
+
};
|
|
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);
|
|
470
166
|
}
|
|
471
167
|
/** Get/set progress (0..1) */
|
|
472
168
|
get progress() {
|
|
@@ -476,13 +172,13 @@ class ProgressBar extends pixi_js.Container {
|
|
|
476
172
|
this._progress = Math.max(0, Math.min(1, value));
|
|
477
173
|
if (!this._config.animated) {
|
|
478
174
|
this._displayedProgress = this._progress;
|
|
479
|
-
this.
|
|
175
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
480
176
|
}
|
|
481
177
|
}
|
|
482
178
|
/**
|
|
483
179
|
* Call each frame if animated is true.
|
|
484
180
|
*/
|
|
485
|
-
update(
|
|
181
|
+
update(_dt) {
|
|
486
182
|
if (!this._config.animated)
|
|
487
183
|
return;
|
|
488
184
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -491,35 +187,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
491
187
|
}
|
|
492
188
|
this._displayedProgress +=
|
|
493
189
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
494
|
-
this.
|
|
495
|
-
}
|
|
496
|
-
drawTrack() {
|
|
497
|
-
const { width, height, borderRadius, trackColor } = this._config;
|
|
498
|
-
this._track.clear();
|
|
499
|
-
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
500
|
-
}
|
|
501
|
-
drawBorder() {
|
|
502
|
-
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
503
|
-
this._border.clear();
|
|
504
|
-
this._border
|
|
505
|
-
.roundRect(0, 0, width, height, borderRadius)
|
|
506
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
507
|
-
}
|
|
508
|
-
drawFill(progress) {
|
|
509
|
-
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
510
|
-
const innerWidth = width - borderWidth * 2;
|
|
511
|
-
const innerHeight = height - borderWidth * 2;
|
|
512
|
-
const fillWidth = Math.max(0, innerWidth * progress);
|
|
513
|
-
this._fill.clear();
|
|
514
|
-
if (fillWidth > 0) {
|
|
515
|
-
this._fill.x = borderWidth;
|
|
516
|
-
this._fill.y = borderWidth;
|
|
517
|
-
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
518
|
-
// Highlight
|
|
519
|
-
this._fill
|
|
520
|
-
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
521
|
-
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
522
|
-
}
|
|
190
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
523
191
|
}
|
|
524
192
|
}
|
|
525
193
|
|
|
@@ -615,7 +283,10 @@ class Label extends pixi_js.Container {
|
|
|
615
283
|
}
|
|
616
284
|
|
|
617
285
|
/**
|
|
618
|
-
* 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.
|
|
619
290
|
*
|
|
620
291
|
* @example
|
|
621
292
|
* ```ts
|
|
@@ -630,75 +301,148 @@ class Label extends pixi_js.Container {
|
|
|
630
301
|
* });
|
|
631
302
|
* ```
|
|
632
303
|
*/
|
|
633
|
-
class Panel extends
|
|
634
|
-
|
|
635
|
-
_content;
|
|
636
|
-
_config;
|
|
304
|
+
class Panel extends components.LayoutContainer {
|
|
305
|
+
_panelConfig;
|
|
637
306
|
constructor(config = {}) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
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,
|
|
644
312
|
...config,
|
|
645
313
|
};
|
|
646
|
-
//
|
|
314
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
315
|
+
let customBackground;
|
|
647
316
|
if (config.nineSliceTexture) {
|
|
648
317
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
649
318
|
? pixi_js.Texture.from(config.nineSliceTexture)
|
|
650
319
|
: config.nineSliceTexture;
|
|
651
320
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
652
|
-
|
|
321
|
+
const nineSlice = new pixi_js.NineSliceSprite({
|
|
653
322
|
texture,
|
|
654
323
|
leftWidth: left,
|
|
655
324
|
topHeight: top,
|
|
656
325
|
rightWidth: right,
|
|
657
326
|
bottomHeight: bottom,
|
|
658
327
|
});
|
|
659
|
-
|
|
660
|
-
|
|
328
|
+
nineSlice.width = resolvedConfig.width;
|
|
329
|
+
nineSlice.height = resolvedConfig.height;
|
|
330
|
+
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
331
|
+
customBackground = nineSlice;
|
|
332
|
+
}
|
|
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
|
+
}
|
|
661
350
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
this.
|
|
351
|
+
this.layout = layoutStyles;
|
|
352
|
+
if (!config.nineSliceTexture) {
|
|
353
|
+
this.background.alpha = resolvedConfig.backgroundAlpha;
|
|
665
354
|
}
|
|
666
|
-
this._bg.alpha = this._config.backgroundAlpha;
|
|
667
|
-
this.addChild(this._bg);
|
|
668
|
-
// Content container with padding
|
|
669
|
-
this._content = new pixi_js.Container();
|
|
670
|
-
this._content.x = this._config.padding;
|
|
671
|
-
this._content.y = this._config.padding;
|
|
672
|
-
this.addChild(this._content);
|
|
673
355
|
}
|
|
674
|
-
/**
|
|
356
|
+
/** Access the content container (children added here participate in layout) */
|
|
675
357
|
get content() {
|
|
676
|
-
return this.
|
|
358
|
+
return this.overflowContainer;
|
|
677
359
|
}
|
|
678
360
|
/** Resize the panel */
|
|
679
361
|
setSize(width, height) {
|
|
680
|
-
this.
|
|
681
|
-
this.
|
|
682
|
-
|
|
683
|
-
this.drawGraphicsBg();
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
this._bg.width = width;
|
|
687
|
-
this._bg.height = height;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
drawGraphicsBg() {
|
|
691
|
-
const bg = this._bg;
|
|
692
|
-
const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
|
|
693
|
-
bg.clear();
|
|
694
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
695
|
-
if (borderColor !== undefined && borderWidth) {
|
|
696
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0)
|
|
697
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
698
|
-
}
|
|
362
|
+
this._panelConfig.width = width;
|
|
363
|
+
this._panelConfig.height = height;
|
|
364
|
+
this._layout?.setStyle({ width, height });
|
|
699
365
|
}
|
|
700
366
|
}
|
|
701
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
|
+
|
|
702
446
|
/**
|
|
703
447
|
* Reactive balance display component.
|
|
704
448
|
*
|
|
@@ -783,7 +527,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
783
527
|
this.updateDisplay();
|
|
784
528
|
}
|
|
785
529
|
async animateValue(from, to) {
|
|
786
|
-
// Cancel any ongoing animation
|
|
787
530
|
if (this._animating) {
|
|
788
531
|
this._animationCancelled = true;
|
|
789
532
|
}
|
|
@@ -793,7 +536,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
793
536
|
const startTime = Date.now();
|
|
794
537
|
return new Promise((resolve) => {
|
|
795
538
|
const tick = () => {
|
|
796
|
-
// If cancelled by a newer animation, stop immediately
|
|
797
539
|
if (this._animationCancelled) {
|
|
798
540
|
this._animating = false;
|
|
799
541
|
resolve();
|
|
@@ -868,66 +610,261 @@ class WinDisplay extends pixi_js.Container {
|
|
|
868
610
|
this.visible = false;
|
|
869
611
|
}
|
|
870
612
|
/**
|
|
871
|
-
* Show a win with countup animation.
|
|
872
|
-
*
|
|
873
|
-
* @param amount - Win amount
|
|
874
|
-
* @returns Promise that resolves when the animation completes
|
|
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
|
+
});
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Animate properties from given values to current values.
|
|
730
|
+
*/
|
|
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.
|
|
875
753
|
*/
|
|
876
|
-
|
|
877
|
-
this.visible = true;
|
|
878
|
-
this._cancelCountup = false;
|
|
879
|
-
this.alpha = 1;
|
|
880
|
-
const duration = this._config.countupDuration;
|
|
881
|
-
const startTime = Date.now();
|
|
882
|
-
// Scale pop
|
|
883
|
-
this.scale.set(0.5);
|
|
754
|
+
static delay(ms) {
|
|
884
755
|
return new Promise((resolve) => {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
const elapsed = Date.now() - startTime;
|
|
892
|
-
const t = Math.min(elapsed / duration, 1);
|
|
893
|
-
const eased = Easing.easeOutCubic(t);
|
|
894
|
-
// Countup
|
|
895
|
-
const current = amount * eased;
|
|
896
|
-
this.displayAmount(current);
|
|
897
|
-
// Scale animation
|
|
898
|
-
const scaleT = Math.min(elapsed / 300, 1);
|
|
899
|
-
const scaleEased = Easing.easeOutBack(scaleT);
|
|
900
|
-
const targetScale = 1;
|
|
901
|
-
this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
|
|
902
|
-
if (t < 1) {
|
|
903
|
-
requestAnimationFrame(tick);
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
this.displayAmount(amount);
|
|
907
|
-
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);
|
|
908
761
|
resolve();
|
|
909
762
|
}
|
|
910
763
|
};
|
|
911
|
-
|
|
764
|
+
pixi_js.Ticker.shared.add(onTick);
|
|
912
765
|
});
|
|
913
766
|
}
|
|
914
767
|
/**
|
|
915
|
-
*
|
|
768
|
+
* Kill all tweens on a target.
|
|
916
769
|
*/
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
});
|
|
921
778
|
}
|
|
922
779
|
/**
|
|
923
|
-
*
|
|
780
|
+
* Kill all active tweens.
|
|
924
781
|
*/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
782
|
+
static killAll() {
|
|
783
|
+
for (const tw of Tween._tweens) {
|
|
784
|
+
tw.resolve();
|
|
785
|
+
}
|
|
786
|
+
Tween._tweens.length = 0;
|
|
928
787
|
}
|
|
929
|
-
|
|
930
|
-
|
|
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;
|
|
931
868
|
}
|
|
932
869
|
}
|
|
933
870
|
|
|
@@ -935,6 +872,8 @@ class WinDisplay extends pixi_js.Container {
|
|
|
935
872
|
* Modal overlay component.
|
|
936
873
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
937
874
|
*
|
|
875
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
876
|
+
*
|
|
938
877
|
* @example
|
|
939
878
|
* ```ts
|
|
940
879
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -953,11 +892,10 @@ class Modal extends pixi_js.Container {
|
|
|
953
892
|
constructor(config = {}) {
|
|
954
893
|
super();
|
|
955
894
|
this._config = {
|
|
956
|
-
overlayColor: 0x000000,
|
|
957
|
-
overlayAlpha: 0.7,
|
|
958
|
-
closeOnOverlay: true,
|
|
959
|
-
animationDuration: 300,
|
|
960
|
-
...config,
|
|
895
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
896
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
897
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
898
|
+
animationDuration: config.animationDuration ?? 300,
|
|
961
899
|
};
|
|
962
900
|
// Overlay
|
|
963
901
|
this._overlay = new pixi_js.Graphics();
|
|
@@ -1025,6 +963,8 @@ const TOAST_COLORS = {
|
|
|
1025
963
|
/**
|
|
1026
964
|
* Toast notification component for displaying transient messages.
|
|
1027
965
|
*
|
|
966
|
+
* Uses `@pixi/layout` LayoutContainer for auto-sized background.
|
|
967
|
+
*
|
|
1028
968
|
* @example
|
|
1029
969
|
* ```ts
|
|
1030
970
|
* const toast = new Toast();
|
|
@@ -1040,11 +980,10 @@ class Toast extends pixi_js.Container {
|
|
|
1040
980
|
constructor(config = {}) {
|
|
1041
981
|
super();
|
|
1042
982
|
this._config = {
|
|
1043
|
-
duration: 3000,
|
|
1044
|
-
bottomOffset: 60,
|
|
1045
|
-
...config,
|
|
983
|
+
duration: config.duration ?? 3000,
|
|
984
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
1046
985
|
};
|
|
1047
|
-
this._bg = new
|
|
986
|
+
this._bg = new components.LayoutContainer();
|
|
1048
987
|
this.addChild(this._bg);
|
|
1049
988
|
this._text = new pixi_js.Text({
|
|
1050
989
|
text: '',
|
|
@@ -1062,7 +1001,6 @@ class Toast extends pixi_js.Container {
|
|
|
1062
1001
|
* Show a toast message.
|
|
1063
1002
|
*/
|
|
1064
1003
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
1065
|
-
// Clear previous dismiss
|
|
1066
1004
|
if (this._dismissTimeout) {
|
|
1067
1005
|
clearTimeout(this._dismissTimeout);
|
|
1068
1006
|
}
|
|
@@ -1071,10 +1009,16 @@ class Toast extends pixi_js.Container {
|
|
|
1071
1009
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
1072
1010
|
const height = 44;
|
|
1073
1011
|
const radius = 8;
|
|
1074
|
-
|
|
1075
|
-
this._bg.
|
|
1076
|
-
|
|
1077
|
-
|
|
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;
|
|
1078
1022
|
// Position
|
|
1079
1023
|
if (viewWidth && viewHeight) {
|
|
1080
1024
|
this.x = viewWidth / 2;
|
|
@@ -1083,9 +1027,7 @@ class Toast extends pixi_js.Container {
|
|
|
1083
1027
|
this.visible = true;
|
|
1084
1028
|
this.alpha = 0;
|
|
1085
1029
|
this.y += 20;
|
|
1086
|
-
// Animate in
|
|
1087
1030
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
1088
|
-
// Auto-dismiss
|
|
1089
1031
|
if (this._config.duration > 0) {
|
|
1090
1032
|
this._dismissTimeout = setTimeout(() => {
|
|
1091
1033
|
this.dismiss();
|
|
@@ -1107,8 +1049,48 @@ class Toast extends pixi_js.Container {
|
|
|
1107
1049
|
}
|
|
1108
1050
|
}
|
|
1109
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
|
+
}
|
|
1110
1092
|
/**
|
|
1111
|
-
* Responsive layout container
|
|
1093
|
+
* Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
|
|
1112
1094
|
*
|
|
1113
1095
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
1114
1096
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -1131,47 +1113,44 @@ class Toast extends pixi_js.Container {
|
|
|
1131
1113
|
* toolbar.addItem(betLabel);
|
|
1132
1114
|
* scene.container.addChild(toolbar);
|
|
1133
1115
|
*
|
|
1134
|
-
* // On resize, update layout position relative to viewport
|
|
1135
1116
|
* toolbar.updateViewport(width, height);
|
|
1136
1117
|
* ```
|
|
1137
1118
|
*/
|
|
1138
1119
|
class Layout extends pixi_js.Container {
|
|
1139
|
-
|
|
1120
|
+
_layoutConfig;
|
|
1140
1121
|
_padding;
|
|
1141
1122
|
_anchor;
|
|
1142
1123
|
_maxWidth;
|
|
1143
1124
|
_breakpoints;
|
|
1144
|
-
_content;
|
|
1145
1125
|
_items = [];
|
|
1146
1126
|
_viewportWidth = 0;
|
|
1147
1127
|
_viewportHeight = 0;
|
|
1148
1128
|
constructor(config = {}) {
|
|
1149
1129
|
super();
|
|
1150
|
-
this.
|
|
1130
|
+
this._layoutConfig = {
|
|
1151
1131
|
direction: config.direction ?? 'vertical',
|
|
1152
1132
|
gap: config.gap ?? 0,
|
|
1153
1133
|
alignment: config.alignment ?? 'start',
|
|
1154
1134
|
autoLayout: config.autoLayout ?? true,
|
|
1155
1135
|
columns: config.columns ?? 2,
|
|
1156
1136
|
};
|
|
1157
|
-
this._padding =
|
|
1137
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
1158
1138
|
this._anchor = config.anchor ?? 'top-left';
|
|
1159
1139
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
1160
|
-
// Sort breakpoints by width ascending for correct resolution
|
|
1161
1140
|
this._breakpoints = config.breakpoints
|
|
1162
1141
|
? Object.entries(config.breakpoints)
|
|
1163
1142
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
1164
1143
|
.sort((a, b) => a[0] - b[0])
|
|
1165
1144
|
: [];
|
|
1166
|
-
this.
|
|
1167
|
-
this.addChild(this._content);
|
|
1145
|
+
this.applyLayoutStyles();
|
|
1168
1146
|
}
|
|
1169
1147
|
/** Add an item to the layout */
|
|
1170
1148
|
addItem(child) {
|
|
1171
1149
|
this._items.push(child);
|
|
1172
|
-
this.
|
|
1173
|
-
if (this.
|
|
1174
|
-
this.
|
|
1150
|
+
this.addChild(child);
|
|
1151
|
+
if (this._layoutConfig.direction === 'grid') {
|
|
1152
|
+
this.applyGridChildWidth(child);
|
|
1153
|
+
}
|
|
1175
1154
|
return this;
|
|
1176
1155
|
}
|
|
1177
1156
|
/** Remove an item from the layout */
|
|
@@ -1179,20 +1158,16 @@ class Layout extends pixi_js.Container {
|
|
|
1179
1158
|
const idx = this._items.indexOf(child);
|
|
1180
1159
|
if (idx !== -1) {
|
|
1181
1160
|
this._items.splice(idx, 1);
|
|
1182
|
-
this.
|
|
1183
|
-
if (this._config.autoLayout)
|
|
1184
|
-
this.layout();
|
|
1161
|
+
this.removeChild(child);
|
|
1185
1162
|
}
|
|
1186
1163
|
return this;
|
|
1187
1164
|
}
|
|
1188
1165
|
/** Remove all items */
|
|
1189
1166
|
clearItems() {
|
|
1190
1167
|
for (const item of this._items) {
|
|
1191
|
-
this.
|
|
1168
|
+
this.removeChild(item);
|
|
1192
1169
|
}
|
|
1193
1170
|
this._items.length = 0;
|
|
1194
|
-
if (this._config.autoLayout)
|
|
1195
|
-
this.layout();
|
|
1196
1171
|
return this;
|
|
1197
1172
|
}
|
|
1198
1173
|
/** Get all layout items */
|
|
@@ -1206,129 +1181,49 @@ class Layout extends pixi_js.Container {
|
|
|
1206
1181
|
updateViewport(width, height) {
|
|
1207
1182
|
this._viewportWidth = width;
|
|
1208
1183
|
this._viewportHeight = height;
|
|
1209
|
-
this.
|
|
1184
|
+
this.applyLayoutStyles();
|
|
1185
|
+
this.applyAnchor();
|
|
1210
1186
|
}
|
|
1211
|
-
|
|
1212
|
-
* Recalculate layout positions of all children.
|
|
1213
|
-
*/
|
|
1214
|
-
layout() {
|
|
1215
|
-
if (this._items.length === 0)
|
|
1216
|
-
return;
|
|
1217
|
-
// Resolve effective config (apply breakpoint overrides)
|
|
1187
|
+
applyLayoutStyles() {
|
|
1218
1188
|
const effective = this.resolveConfig();
|
|
1219
|
-
const
|
|
1220
|
-
const
|
|
1221
|
-
const alignment = effective.alignment ?? this.
|
|
1222
|
-
|
|
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;
|
|
1223
1193
|
const padding = effective.padding !== undefined
|
|
1224
|
-
?
|
|
1194
|
+
? normalizePadding(effective.padding)
|
|
1225
1195
|
: this._padding;
|
|
1226
1196
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
case 'vertical':
|
|
1233
|
-
this.layoutLinear('y', 'x', gap, alignment, pt, pl);
|
|
1234
|
-
break;
|
|
1235
|
-
case 'grid':
|
|
1236
|
-
this.layoutGrid(columns, gap, alignment, pl, pt);
|
|
1237
|
-
break;
|
|
1238
|
-
case 'wrap':
|
|
1239
|
-
this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
|
|
1240
|
-
break;
|
|
1241
|
-
}
|
|
1242
|
-
// Apply anchor positioning relative to viewport
|
|
1243
|
-
this.applyAnchor(effective.anchor ?? this._anchor);
|
|
1244
|
-
}
|
|
1245
|
-
// ─── Private layout helpers ────────────────────────────
|
|
1246
|
-
layoutLinear(mainAxis, crossAxis, gap, alignment, mainOffset, crossOffset) {
|
|
1247
|
-
let pos = mainOffset;
|
|
1248
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
1249
|
-
const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
|
|
1250
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
1251
|
-
const item = this._items[i];
|
|
1252
|
-
const size = sizes[i];
|
|
1253
|
-
item[mainAxis] = pos;
|
|
1254
|
-
// Cross-axis alignment
|
|
1255
|
-
const itemCross = crossAxis === 'x' ? size.width : size.height;
|
|
1256
|
-
switch (alignment) {
|
|
1257
|
-
case 'start':
|
|
1258
|
-
item[crossAxis] = crossOffset;
|
|
1259
|
-
break;
|
|
1260
|
-
case 'center':
|
|
1261
|
-
item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
|
|
1262
|
-
break;
|
|
1263
|
-
case 'end':
|
|
1264
|
-
item[crossAxis] = crossOffset + maxCross - itemCross;
|
|
1265
|
-
break;
|
|
1266
|
-
case 'stretch':
|
|
1267
|
-
item[crossAxis] = crossOffset;
|
|
1268
|
-
// Note: stretch doesn't resize children — that's up to the item
|
|
1269
|
-
break;
|
|
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);
|
|
1270
1202
|
}
|
|
1271
|
-
const mainSize = mainAxis === 'x' ? size.width : size.height;
|
|
1272
|
-
pos += mainSize + gap;
|
|
1273
1203
|
}
|
|
1274
1204
|
}
|
|
1275
|
-
|
|
1276
|
-
const
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
1282
|
-
const item = this._items[i];
|
|
1283
|
-
const col = i % columns;
|
|
1284
|
-
const row = Math.floor(i / columns);
|
|
1285
|
-
const size = sizes[i];
|
|
1286
|
-
// X alignment within cell
|
|
1287
|
-
switch (alignment) {
|
|
1288
|
-
case 'center':
|
|
1289
|
-
item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
|
|
1290
|
-
break;
|
|
1291
|
-
case 'end':
|
|
1292
|
-
item.x = offsetX + col * cellW + maxItemWidth - size.width;
|
|
1293
|
-
break;
|
|
1294
|
-
default:
|
|
1295
|
-
item.x = offsetX + col * cellW;
|
|
1296
|
-
}
|
|
1297
|
-
item.y = offsetY + row * cellH;
|
|
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 });
|
|
1298
1211
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
let x = offsetX;
|
|
1302
|
-
let y = offsetY;
|
|
1303
|
-
let rowHeight = 0;
|
|
1304
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
1305
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
1306
|
-
const item = this._items[i];
|
|
1307
|
-
const size = sizes[i];
|
|
1308
|
-
// Check if item fits in current row
|
|
1309
|
-
if (x + size.width > maxWidth + offsetX && x > offsetX) {
|
|
1310
|
-
// Wrap to next row
|
|
1311
|
-
x = offsetX;
|
|
1312
|
-
y += rowHeight + gap;
|
|
1313
|
-
rowHeight = 0;
|
|
1314
|
-
}
|
|
1315
|
-
item.x = x;
|
|
1316
|
-
item.y = y;
|
|
1317
|
-
x += size.width + gap;
|
|
1318
|
-
rowHeight = Math.max(rowHeight, size.height);
|
|
1212
|
+
else {
|
|
1213
|
+
child.layout = { width: pct };
|
|
1319
1214
|
}
|
|
1320
1215
|
}
|
|
1321
|
-
applyAnchor(
|
|
1216
|
+
applyAnchor() {
|
|
1217
|
+
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
1322
1218
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
1323
1219
|
return;
|
|
1324
|
-
const bounds = this.
|
|
1325
|
-
const contentW = bounds.width;
|
|
1326
|
-
const contentH = bounds.height;
|
|
1220
|
+
const bounds = this.getLocalBounds();
|
|
1221
|
+
const contentW = bounds.width * this.scale.x;
|
|
1222
|
+
const contentH = bounds.height * this.scale.y;
|
|
1327
1223
|
const vw = this._viewportWidth;
|
|
1328
1224
|
const vh = this._viewportHeight;
|
|
1329
1225
|
let anchorX = 0;
|
|
1330
1226
|
let anchorY = 0;
|
|
1331
|
-
// Horizontal
|
|
1332
1227
|
if (anchor.includes('left')) {
|
|
1333
1228
|
anchorX = 0;
|
|
1334
1229
|
}
|
|
@@ -1336,10 +1231,8 @@ class Layout extends pixi_js.Container {
|
|
|
1336
1231
|
anchorX = vw - contentW;
|
|
1337
1232
|
}
|
|
1338
1233
|
else {
|
|
1339
|
-
// center
|
|
1340
1234
|
anchorX = (vw - contentW) / 2;
|
|
1341
1235
|
}
|
|
1342
|
-
// Vertical
|
|
1343
1236
|
if (anchor.startsWith('top')) {
|
|
1344
1237
|
anchorY = 0;
|
|
1345
1238
|
}
|
|
@@ -1347,44 +1240,34 @@ class Layout extends pixi_js.Container {
|
|
|
1347
1240
|
anchorY = vh - contentH;
|
|
1348
1241
|
}
|
|
1349
1242
|
else {
|
|
1350
|
-
// center
|
|
1351
1243
|
anchorY = (vh - contentH) / 2;
|
|
1352
1244
|
}
|
|
1353
|
-
|
|
1354
|
-
this.
|
|
1355
|
-
this.y = anchorY - bounds.y;
|
|
1245
|
+
this.x = anchorX - bounds.x * this.scale.x;
|
|
1246
|
+
this.y = anchorY - bounds.y * this.scale.y;
|
|
1356
1247
|
}
|
|
1357
1248
|
resolveConfig() {
|
|
1358
1249
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
1359
1250
|
return {};
|
|
1360
1251
|
}
|
|
1361
|
-
// Find the largest breakpoint that's ≤ current viewport width
|
|
1362
|
-
let resolved = {};
|
|
1363
1252
|
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
1364
1253
|
if (this._viewportWidth <= maxWidth) {
|
|
1365
|
-
|
|
1366
|
-
break;
|
|
1254
|
+
return overrides;
|
|
1367
1255
|
}
|
|
1368
1256
|
}
|
|
1369
|
-
return
|
|
1370
|
-
}
|
|
1371
|
-
getItemSize(item) {
|
|
1372
|
-
const bounds = item.getBounds();
|
|
1373
|
-
return { width: bounds.width, height: bounds.height };
|
|
1374
|
-
}
|
|
1375
|
-
static normalizePadding(padding) {
|
|
1376
|
-
if (typeof padding === 'number') {
|
|
1377
|
-
return [padding, padding, padding, padding];
|
|
1378
|
-
}
|
|
1379
|
-
return padding;
|
|
1257
|
+
return {};
|
|
1380
1258
|
}
|
|
1381
1259
|
}
|
|
1382
1260
|
|
|
1261
|
+
const DIRECTION_MAP = {
|
|
1262
|
+
vertical: 'vertical',
|
|
1263
|
+
horizontal: 'horizontal',
|
|
1264
|
+
both: 'bidirectional',
|
|
1265
|
+
};
|
|
1383
1266
|
/**
|
|
1384
|
-
* Scrollable container
|
|
1267
|
+
* Scrollable container powered by `@pixi/ui` ScrollBox.
|
|
1385
1268
|
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1269
|
+
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
1270
|
+
* dynamic rendering optimization for off-screen items.
|
|
1388
1271
|
*
|
|
1389
1272
|
* @example
|
|
1390
1273
|
* ```ts
|
|
@@ -1392,438 +1275,86 @@ class Layout extends pixi_js.Container {
|
|
|
1392
1275
|
* width: 600,
|
|
1393
1276
|
* height: 400,
|
|
1394
1277
|
* direction: 'vertical',
|
|
1395
|
-
*
|
|
1396
|
-
* elasticity: 0.3,
|
|
1278
|
+
* elementsMargin: 8,
|
|
1397
1279
|
* });
|
|
1398
1280
|
*
|
|
1399
|
-
* // Add content taller than 400px
|
|
1400
|
-
* const list = new Container();
|
|
1401
1281
|
* for (let i = 0; i < 50; i++) {
|
|
1402
|
-
*
|
|
1403
|
-
* row.y = i * 40;
|
|
1404
|
-
* list.addChild(row);
|
|
1282
|
+
* scroll.addItem(createRow(i));
|
|
1405
1283
|
* }
|
|
1406
|
-
* scroll.setContent(list);
|
|
1407
1284
|
*
|
|
1408
1285
|
* scene.container.addChild(scroll);
|
|
1409
1286
|
* ```
|
|
1410
1287
|
*/
|
|
1411
|
-
class ScrollContainer extends
|
|
1412
|
-
|
|
1413
|
-
_viewport;
|
|
1414
|
-
_content = null;
|
|
1415
|
-
_mask;
|
|
1416
|
-
_bg;
|
|
1417
|
-
_scrollbarV = null;
|
|
1418
|
-
_scrollbarH = null;
|
|
1419
|
-
_scrollbarFadeTimeout = null;
|
|
1420
|
-
// Scroll state
|
|
1421
|
-
_scrollX = 0;
|
|
1422
|
-
_scrollY = 0;
|
|
1423
|
-
_velocityX = 0;
|
|
1424
|
-
_velocityY = 0;
|
|
1425
|
-
_isDragging = false;
|
|
1426
|
-
_dragStart = { x: 0, y: 0 };
|
|
1427
|
-
_scrollStart = { x: 0, y: 0 };
|
|
1428
|
-
_lastDragPos = { x: 0, y: 0 };
|
|
1429
|
-
_lastDragTime = 0;
|
|
1430
|
-
_isAnimating = false;
|
|
1431
|
-
_animationFrame = null;
|
|
1288
|
+
class ScrollContainer extends ui.ScrollBox {
|
|
1289
|
+
_scrollConfig;
|
|
1432
1290
|
constructor(config) {
|
|
1433
|
-
|
|
1434
|
-
this._config = {
|
|
1291
|
+
const options = {
|
|
1435
1292
|
width: config.width,
|
|
1436
1293
|
height: config.height,
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
snapSize: config.snapSize ?? 0,
|
|
1445
|
-
borderRadius: config.borderRadius ?? 0,
|
|
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,
|
|
1446
1301
|
};
|
|
1447
|
-
// Background
|
|
1448
|
-
this._bg = new pixi_js.Graphics();
|
|
1449
1302
|
if (config.backgroundColor !== undefined) {
|
|
1450
|
-
|
|
1451
|
-
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
1452
|
-
}
|
|
1453
|
-
this.addChild(this._bg);
|
|
1454
|
-
// Viewport (masked area)
|
|
1455
|
-
this._viewport = new pixi_js.Container();
|
|
1456
|
-
this.addChild(this._viewport);
|
|
1457
|
-
// Mask
|
|
1458
|
-
this._mask = new pixi_js.Graphics();
|
|
1459
|
-
this._mask.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
1460
|
-
.fill(0xffffff);
|
|
1461
|
-
this.addChild(this._mask);
|
|
1462
|
-
this._viewport.mask = this._mask;
|
|
1463
|
-
// Scrollbars
|
|
1464
|
-
if (this._config.showScrollbar) {
|
|
1465
|
-
if (this._config.direction !== 'horizontal') {
|
|
1466
|
-
this._scrollbarV = new pixi_js.Graphics();
|
|
1467
|
-
this._scrollbarV.alpha = 0;
|
|
1468
|
-
this.addChild(this._scrollbarV);
|
|
1469
|
-
}
|
|
1470
|
-
if (this._config.direction !== 'vertical') {
|
|
1471
|
-
this._scrollbarH = new pixi_js.Graphics();
|
|
1472
|
-
this._scrollbarH.alpha = 0;
|
|
1473
|
-
this.addChild(this._scrollbarH);
|
|
1474
|
-
}
|
|
1303
|
+
options.background = config.backgroundColor;
|
|
1475
1304
|
}
|
|
1476
|
-
|
|
1477
|
-
this.
|
|
1478
|
-
this.cursor = 'grab';
|
|
1479
|
-
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
1480
|
-
this.on('pointerdown', this.onPointerDown);
|
|
1481
|
-
this.on('pointermove', this.onPointerMove);
|
|
1482
|
-
this.on('pointerup', this.onPointerUp);
|
|
1483
|
-
this.on('pointerupoutside', this.onPointerUp);
|
|
1484
|
-
this.on('wheel', this.onWheel);
|
|
1305
|
+
super(options);
|
|
1306
|
+
this._scrollConfig = config;
|
|
1485
1307
|
}
|
|
1486
1308
|
/** Set scrollable content. Replaces any existing content. */
|
|
1487
1309
|
setContent(content) {
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
+
}
|
|
1490
1316
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
this.applyScroll();
|
|
1496
|
-
}
|
|
1497
|
-
/** Get the content container */
|
|
1498
|
-
get content() {
|
|
1499
|
-
return this._content;
|
|
1500
|
-
}
|
|
1501
|
-
/** Scroll to a specific position (in content coordinates) */
|
|
1502
|
-
scrollTo(x, y, animate = true) {
|
|
1503
|
-
if (!animate) {
|
|
1504
|
-
this._scrollX = x;
|
|
1505
|
-
this._scrollY = y;
|
|
1506
|
-
this.clampScroll();
|
|
1507
|
-
this.applyScroll();
|
|
1508
|
-
return;
|
|
1317
|
+
// Add all children from the content container
|
|
1318
|
+
const children = [...content.children];
|
|
1319
|
+
if (children.length > 0) {
|
|
1320
|
+
this.addItems(children);
|
|
1509
1321
|
}
|
|
1510
|
-
|
|
1322
|
+
}
|
|
1323
|
+
/** Add a single item */
|
|
1324
|
+
addItem(...items) {
|
|
1325
|
+
this.addItems(items);
|
|
1326
|
+
return items[0];
|
|
1511
1327
|
}
|
|
1512
1328
|
/** Scroll to make a specific item/child visible */
|
|
1513
1329
|
scrollToItem(index) {
|
|
1514
|
-
|
|
1515
|
-
const pos = index * this._config.snapSize;
|
|
1516
|
-
if (this._config.direction === 'horizontal') {
|
|
1517
|
-
this.scrollTo(pos, this._scrollY);
|
|
1518
|
-
}
|
|
1519
|
-
else {
|
|
1520
|
-
this.scrollTo(this._scrollX, pos);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1330
|
+
this.scrollTo(index);
|
|
1523
1331
|
}
|
|
1524
1332
|
/** Current scroll position */
|
|
1525
1333
|
get scrollPosition() {
|
|
1526
|
-
return { x: this.
|
|
1527
|
-
}
|
|
1528
|
-
/** Resize the scroll viewport */
|
|
1529
|
-
resize(width, height) {
|
|
1530
|
-
this._config.width = width;
|
|
1531
|
-
this._config.height = height;
|
|
1532
|
-
// Redraw mask and background
|
|
1533
|
-
this._mask.clear();
|
|
1534
|
-
this._mask.roundRect(0, 0, width, height, this._config.borderRadius).fill(0xffffff);
|
|
1535
|
-
this._bg.clear();
|
|
1536
|
-
this.hitArea = { contains: (x, y) => x >= 0 && x <= width && y >= 0 && y <= height };
|
|
1537
|
-
this.clampScroll();
|
|
1538
|
-
this.applyScroll();
|
|
1539
|
-
}
|
|
1540
|
-
/** Destroy and clean up */
|
|
1541
|
-
destroy(options) {
|
|
1542
|
-
this.stopAnimation();
|
|
1543
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
1544
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
1545
|
-
}
|
|
1546
|
-
this.off('pointerdown', this.onPointerDown);
|
|
1547
|
-
this.off('pointermove', this.onPointerMove);
|
|
1548
|
-
this.off('pointerup', this.onPointerUp);
|
|
1549
|
-
this.off('pointerupoutside', this.onPointerUp);
|
|
1550
|
-
this.off('wheel', this.onWheel);
|
|
1551
|
-
super.destroy(options);
|
|
1552
|
-
}
|
|
1553
|
-
// ─── Scroll mechanics ─────────────────────────────────
|
|
1554
|
-
get contentWidth() {
|
|
1555
|
-
if (!this._content)
|
|
1556
|
-
return 0;
|
|
1557
|
-
const bounds = this._content.getBounds();
|
|
1558
|
-
return bounds.width;
|
|
1559
|
-
}
|
|
1560
|
-
get contentHeight() {
|
|
1561
|
-
if (!this._content)
|
|
1562
|
-
return 0;
|
|
1563
|
-
const bounds = this._content.getBounds();
|
|
1564
|
-
return bounds.height;
|
|
1565
|
-
}
|
|
1566
|
-
get maxScrollX() {
|
|
1567
|
-
return Math.max(0, this.contentWidth - this._config.width);
|
|
1568
|
-
}
|
|
1569
|
-
get maxScrollY() {
|
|
1570
|
-
return Math.max(0, this.contentHeight - this._config.height);
|
|
1571
|
-
}
|
|
1572
|
-
canScrollX() {
|
|
1573
|
-
return this._config.direction === 'horizontal' || this._config.direction === 'both';
|
|
1574
|
-
}
|
|
1575
|
-
canScrollY() {
|
|
1576
|
-
return this._config.direction === 'vertical' || this._config.direction === 'both';
|
|
1577
|
-
}
|
|
1578
|
-
clampScroll() {
|
|
1579
|
-
if (this.canScrollX()) {
|
|
1580
|
-
this._scrollX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
1581
|
-
}
|
|
1582
|
-
else {
|
|
1583
|
-
this._scrollX = 0;
|
|
1584
|
-
}
|
|
1585
|
-
if (this.canScrollY()) {
|
|
1586
|
-
this._scrollY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
1587
|
-
}
|
|
1588
|
-
else {
|
|
1589
|
-
this._scrollY = 0;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
applyScroll() {
|
|
1593
|
-
if (!this._content)
|
|
1594
|
-
return;
|
|
1595
|
-
this._content.x = -this._scrollX;
|
|
1596
|
-
this._content.y = -this._scrollY;
|
|
1597
|
-
this.updateScrollbars();
|
|
1598
|
-
}
|
|
1599
|
-
// ─── Input handlers ────────────────────────────────────
|
|
1600
|
-
onPointerDown = (e) => {
|
|
1601
|
-
this._isDragging = true;
|
|
1602
|
-
this._isAnimating = false;
|
|
1603
|
-
this.stopAnimation();
|
|
1604
|
-
this.cursor = 'grabbing';
|
|
1605
|
-
const local = e.getLocalPosition(this);
|
|
1606
|
-
this._dragStart = { x: local.x, y: local.y };
|
|
1607
|
-
this._scrollStart = { x: this._scrollX, y: this._scrollY };
|
|
1608
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
1609
|
-
this._lastDragTime = Date.now();
|
|
1610
|
-
this._velocityX = 0;
|
|
1611
|
-
this._velocityY = 0;
|
|
1612
|
-
this.showScrollbars();
|
|
1613
|
-
};
|
|
1614
|
-
onPointerMove = (e) => {
|
|
1615
|
-
if (!this._isDragging)
|
|
1616
|
-
return;
|
|
1617
|
-
const local = e.getLocalPosition(this);
|
|
1618
|
-
const dx = local.x - this._dragStart.x;
|
|
1619
|
-
const dy = local.y - this._dragStart.y;
|
|
1620
|
-
const now = Date.now();
|
|
1621
|
-
const dt = Math.max(1, now - this._lastDragTime);
|
|
1622
|
-
// Calculate velocity for inertia
|
|
1623
|
-
this._velocityX = (local.x - this._lastDragPos.x) / dt * 16; // normalize to ~60fps
|
|
1624
|
-
this._velocityY = (local.y - this._lastDragPos.y) / dt * 16;
|
|
1625
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
1626
|
-
this._lastDragTime = now;
|
|
1627
|
-
// Apply scroll with elasticity for overscroll
|
|
1628
|
-
let newX = this._scrollStart.x - dx;
|
|
1629
|
-
let newY = this._scrollStart.y - dy;
|
|
1630
|
-
const elasticity = this._config.elasticity;
|
|
1631
|
-
if (this.canScrollX()) {
|
|
1632
|
-
if (newX < 0)
|
|
1633
|
-
newX *= elasticity;
|
|
1634
|
-
else if (newX > this.maxScrollX)
|
|
1635
|
-
newX = this.maxScrollX + (newX - this.maxScrollX) * elasticity;
|
|
1636
|
-
this._scrollX = newX;
|
|
1637
|
-
}
|
|
1638
|
-
if (this.canScrollY()) {
|
|
1639
|
-
if (newY < 0)
|
|
1640
|
-
newY *= elasticity;
|
|
1641
|
-
else if (newY > this.maxScrollY)
|
|
1642
|
-
newY = this.maxScrollY + (newY - this.maxScrollY) * elasticity;
|
|
1643
|
-
this._scrollY = newY;
|
|
1644
|
-
}
|
|
1645
|
-
this.applyScroll();
|
|
1646
|
-
};
|
|
1647
|
-
onPointerUp = () => {
|
|
1648
|
-
if (!this._isDragging)
|
|
1649
|
-
return;
|
|
1650
|
-
this._isDragging = false;
|
|
1651
|
-
this.cursor = 'grab';
|
|
1652
|
-
// Start inertia
|
|
1653
|
-
if (Math.abs(this._velocityX) > 0.5 || Math.abs(this._velocityY) > 0.5) {
|
|
1654
|
-
this.startInertia();
|
|
1655
|
-
}
|
|
1656
|
-
else {
|
|
1657
|
-
this.snapAndBounce();
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
onWheel = (e) => {
|
|
1661
|
-
e.preventDefault?.();
|
|
1662
|
-
const delta = e.deltaY ?? 0;
|
|
1663
|
-
const deltaX = e.deltaX ?? 0;
|
|
1664
|
-
if (this.canScrollY()) {
|
|
1665
|
-
this._scrollY += delta * 0.5;
|
|
1666
|
-
}
|
|
1667
|
-
if (this.canScrollX()) {
|
|
1668
|
-
this._scrollX += deltaX * 0.5;
|
|
1669
|
-
}
|
|
1670
|
-
this.clampScroll();
|
|
1671
|
-
this.applyScroll();
|
|
1672
|
-
this.showScrollbars();
|
|
1673
|
-
this.scheduleScrollbarFade();
|
|
1674
|
-
};
|
|
1675
|
-
// ─── Inertia & snap ───────────────────────────────────
|
|
1676
|
-
startInertia() {
|
|
1677
|
-
this._isAnimating = true;
|
|
1678
|
-
const tick = () => {
|
|
1679
|
-
if (!this._isAnimating)
|
|
1680
|
-
return;
|
|
1681
|
-
this._velocityX *= this._config.inertia;
|
|
1682
|
-
this._velocityY *= this._config.inertia;
|
|
1683
|
-
if (this.canScrollX())
|
|
1684
|
-
this._scrollX -= this._velocityX;
|
|
1685
|
-
if (this.canScrollY())
|
|
1686
|
-
this._scrollY -= this._velocityY;
|
|
1687
|
-
// Bounce back if overscrolled
|
|
1688
|
-
let bounced = false;
|
|
1689
|
-
if (this.canScrollX()) {
|
|
1690
|
-
if (this._scrollX < 0) {
|
|
1691
|
-
this._scrollX *= 0.8;
|
|
1692
|
-
bounced = true;
|
|
1693
|
-
}
|
|
1694
|
-
else if (this._scrollX > this.maxScrollX) {
|
|
1695
|
-
this._scrollX = this.maxScrollX + (this._scrollX - this.maxScrollX) * 0.8;
|
|
1696
|
-
bounced = true;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
if (this.canScrollY()) {
|
|
1700
|
-
if (this._scrollY < 0) {
|
|
1701
|
-
this._scrollY *= 0.8;
|
|
1702
|
-
bounced = true;
|
|
1703
|
-
}
|
|
1704
|
-
else if (this._scrollY > this.maxScrollY) {
|
|
1705
|
-
this._scrollY = this.maxScrollY + (this._scrollY - this.maxScrollY) * 0.8;
|
|
1706
|
-
bounced = true;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
this.applyScroll();
|
|
1710
|
-
const speed = Math.abs(this._velocityX) + Math.abs(this._velocityY);
|
|
1711
|
-
if (speed < 0.1 && !bounced) {
|
|
1712
|
-
this._isAnimating = false;
|
|
1713
|
-
this.snapAndBounce();
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
1717
|
-
};
|
|
1718
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
1719
|
-
}
|
|
1720
|
-
snapAndBounce() {
|
|
1721
|
-
// Clamp first
|
|
1722
|
-
let targetX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
1723
|
-
let targetY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
1724
|
-
// Snap
|
|
1725
|
-
if (this._config.snapSize > 0) {
|
|
1726
|
-
if (this.canScrollY()) {
|
|
1727
|
-
targetY = Math.round(targetY / this._config.snapSize) * this._config.snapSize;
|
|
1728
|
-
targetY = Math.max(0, Math.min(targetY, this.maxScrollY));
|
|
1729
|
-
}
|
|
1730
|
-
if (this.canScrollX()) {
|
|
1731
|
-
targetX = Math.round(targetX / this._config.snapSize) * this._config.snapSize;
|
|
1732
|
-
targetX = Math.max(0, Math.min(targetX, this.maxScrollX));
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
if (Math.abs(targetX - this._scrollX) < 0.5 && Math.abs(targetY - this._scrollY) < 0.5) {
|
|
1736
|
-
this._scrollX = targetX;
|
|
1737
|
-
this._scrollY = targetY;
|
|
1738
|
-
this.applyScroll();
|
|
1739
|
-
this.scheduleScrollbarFade();
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
this.animateScrollTo(targetX, targetY);
|
|
1743
|
-
}
|
|
1744
|
-
animateScrollTo(targetX, targetY) {
|
|
1745
|
-
this._isAnimating = true;
|
|
1746
|
-
const startX = this._scrollX;
|
|
1747
|
-
const startY = this._scrollY;
|
|
1748
|
-
const startTime = Date.now();
|
|
1749
|
-
const duration = 300;
|
|
1750
|
-
const tick = () => {
|
|
1751
|
-
if (!this._isAnimating)
|
|
1752
|
-
return;
|
|
1753
|
-
const elapsed = Date.now() - startTime;
|
|
1754
|
-
const t = Math.min(elapsed / duration, 1);
|
|
1755
|
-
// easeOutCubic
|
|
1756
|
-
const eased = 1 - Math.pow(1 - t, 3);
|
|
1757
|
-
this._scrollX = startX + (targetX - startX) * eased;
|
|
1758
|
-
this._scrollY = startY + (targetY - startY) * eased;
|
|
1759
|
-
this.applyScroll();
|
|
1760
|
-
if (t < 1) {
|
|
1761
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
this._isAnimating = false;
|
|
1765
|
-
this.scheduleScrollbarFade();
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
1769
|
-
}
|
|
1770
|
-
stopAnimation() {
|
|
1771
|
-
this._isAnimating = false;
|
|
1772
|
-
if (this._animationFrame !== null) {
|
|
1773
|
-
cancelAnimationFrame(this._animationFrame);
|
|
1774
|
-
this._animationFrame = null;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
// ─── Scrollbars ────────────────────────────────────────
|
|
1778
|
-
updateScrollbars() {
|
|
1779
|
-
const { width, height, scrollbarWidth, scrollbarColor, scrollbarAlpha } = this._config;
|
|
1780
|
-
if (this._scrollbarV && this.canScrollY() && this.contentHeight > height) {
|
|
1781
|
-
const ratio = height / this.contentHeight;
|
|
1782
|
-
const barH = Math.max(20, height * ratio);
|
|
1783
|
-
const barY = (this._scrollY / this.maxScrollY) * (height - barH);
|
|
1784
|
-
this._scrollbarV.clear();
|
|
1785
|
-
this._scrollbarV.roundRect(width - scrollbarWidth - 2, Math.max(0, barY), scrollbarWidth, barH, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
1786
|
-
}
|
|
1787
|
-
if (this._scrollbarH && this.canScrollX() && this.contentWidth > width) {
|
|
1788
|
-
const ratio = width / this.contentWidth;
|
|
1789
|
-
const barW = Math.max(20, width * ratio);
|
|
1790
|
-
const barX = (this._scrollX / this.maxScrollX) * (width - barW);
|
|
1791
|
-
this._scrollbarH.clear();
|
|
1792
|
-
this._scrollbarH.roundRect(Math.max(0, barX), height - scrollbarWidth - 2, barW, scrollbarWidth, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
showScrollbars() {
|
|
1796
|
-
if (this._scrollbarV)
|
|
1797
|
-
this._scrollbarV.alpha = 1;
|
|
1798
|
-
if (this._scrollbarH)
|
|
1799
|
-
this._scrollbarH.alpha = 1;
|
|
1800
|
-
}
|
|
1801
|
-
scheduleScrollbarFade() {
|
|
1802
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
1803
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
1804
|
-
}
|
|
1805
|
-
this._scrollbarFadeTimeout = window.setTimeout(() => {
|
|
1806
|
-
this.fadeScrollbars();
|
|
1807
|
-
}, 1000);
|
|
1808
|
-
}
|
|
1809
|
-
fadeScrollbars() {
|
|
1810
|
-
const duration = 300;
|
|
1811
|
-
const startTime = Date.now();
|
|
1812
|
-
const startAlphaV = this._scrollbarV?.alpha ?? 0;
|
|
1813
|
-
const startAlphaH = this._scrollbarH?.alpha ?? 0;
|
|
1814
|
-
const tick = () => {
|
|
1815
|
-
const t = Math.min((Date.now() - startTime) / duration, 1);
|
|
1816
|
-
if (this._scrollbarV)
|
|
1817
|
-
this._scrollbarV.alpha = startAlphaV * (1 - t);
|
|
1818
|
-
if (this._scrollbarH)
|
|
1819
|
-
this._scrollbarH.alpha = startAlphaH * (1 - t);
|
|
1820
|
-
if (t < 1)
|
|
1821
|
-
requestAnimationFrame(tick);
|
|
1822
|
-
};
|
|
1823
|
-
requestAnimationFrame(tick);
|
|
1334
|
+
return { x: this.scrollX, y: this.scrollY };
|
|
1824
1335
|
}
|
|
1825
1336
|
}
|
|
1826
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
|
+
});
|
|
1827
1358
|
exports.BalanceDisplay = BalanceDisplay;
|
|
1828
1359
|
exports.Button = Button;
|
|
1829
1360
|
exports.Label = Label;
|