@energy8platform/game-engine 0.10.10 → 0.11.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 +185 -74
- package/dist/index.cjs.js +1280 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +362 -46
- package/dist/index.esm.js +1281 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +16 -21
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +16 -21
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2372 -11
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2372 -12
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1553 -632
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +374 -46
- package/dist/ui.esm.js +1553 -634
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +1 -11
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +1 -11
- package/dist/vite.esm.js.map +1 -1
- package/package.json +3 -18
- package/src/index.ts +3 -3
- package/src/lua/LuaEngine.ts +8 -18
- package/src/lua/SimulationRunner.ts +7 -3
- package/src/react/applyProps.ts +86 -0
- package/src/react/extendAll.ts +27 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +222 -0
- package/src/react/reconciler.ts +22 -5
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +479 -0
- package/src/ui/Label.ts +13 -0
- package/src/ui/Layout.ts +86 -87
- package/src/ui/Modal.ts +11 -1
- package/src/ui/Panel.ts +108 -36
- package/src/ui/ProgressBar.ts +85 -31
- package/src/ui/ScrollContainer.ts +397 -45
- package/src/ui/Toast.ts +47 -17
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +5 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
package/dist/ui.cjs.js
CHANGED
|
@@ -1,9 +1,617 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('@pixi/layout');
|
|
4
3
|
var pixi_js = require('pixi.js');
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a ViewInput to a Container instance.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* resolveView('btn-idle') // → Sprite.from('btn-idle')
|
|
11
|
+
* resolveView(someTexture) // → new Sprite(someTexture)
|
|
12
|
+
* resolveView(myCustomContainer) // → myCustomContainer (as-is)
|
|
13
|
+
* resolveView(undefined) // → null
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function resolveView(input) {
|
|
17
|
+
if (input == null)
|
|
18
|
+
return null;
|
|
19
|
+
if (typeof input === 'string')
|
|
20
|
+
return pixi_js.Sprite.from(input);
|
|
21
|
+
if (input instanceof pixi_js.Texture)
|
|
22
|
+
return new pixi_js.Sprite(input);
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
27
|
+
function normalizePadding(p) {
|
|
28
|
+
return typeof p === 'number' ? [p, p, p, p] : p;
|
|
29
|
+
}
|
|
30
|
+
/** Measure a child's size and bounds offset for layout purposes */
|
|
31
|
+
function measureChild(child) {
|
|
32
|
+
const cfg = child._flexConfig;
|
|
33
|
+
if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
|
|
34
|
+
return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
|
|
35
|
+
}
|
|
36
|
+
// For FlexContainers, use their explicit size if set
|
|
37
|
+
if (child instanceof FlexContainer) {
|
|
38
|
+
const fc = child;
|
|
39
|
+
if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
|
|
40
|
+
return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Use localBounds to get the true visual extent and origin offset.
|
|
44
|
+
// This handles children with non-zero anchors (e.g. Button, Label with centered text).
|
|
45
|
+
const bounds = child.getLocalBounds();
|
|
46
|
+
const w = cfg?.layoutWidth ?? bounds.width;
|
|
47
|
+
const h = cfg?.layoutHeight ?? bounds.height;
|
|
48
|
+
return { w, h, ox: bounds.x, oy: bounds.y };
|
|
49
|
+
}
|
|
50
|
+
function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
|
|
51
|
+
if (items.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
// Compute total fixed main size and flex grow total
|
|
54
|
+
let totalFixed = 0;
|
|
55
|
+
let totalGrow = 0;
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const grow = item.child._flexConfig?.flexGrow ?? 0;
|
|
58
|
+
if (grow > 0) {
|
|
59
|
+
totalGrow += grow;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
totalFixed += isRow ? item.w : item.h;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const totalGap = gap * (items.length - 1);
|
|
66
|
+
const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
|
|
67
|
+
// Resolve flex sizes
|
|
68
|
+
if (totalGrow > 0) {
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const grow = item.child._flexConfig?.flexGrow ?? 0;
|
|
71
|
+
if (grow > 0) {
|
|
72
|
+
const flexSize = (grow / totalGrow) * availableForFlex;
|
|
73
|
+
if (isRow) {
|
|
74
|
+
item.w = flexSize;
|
|
75
|
+
item.child.width = flexSize;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
item.h = flexSize;
|
|
79
|
+
item.child.height = flexSize;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Calculate total main size after flex
|
|
85
|
+
let totalMain = totalGap;
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
totalMain += isRow ? item.w : item.h;
|
|
88
|
+
}
|
|
89
|
+
// Justify: compute starting offset and extra spacing
|
|
90
|
+
let mainOffset = 0;
|
|
91
|
+
let extraGap = 0;
|
|
92
|
+
switch (justify) {
|
|
93
|
+
case 'start':
|
|
94
|
+
break;
|
|
95
|
+
case 'center':
|
|
96
|
+
mainOffset = Math.max(0, (mainSize - totalMain) / 2);
|
|
97
|
+
break;
|
|
98
|
+
case 'end':
|
|
99
|
+
mainOffset = Math.max(0, mainSize - totalMain);
|
|
100
|
+
break;
|
|
101
|
+
case 'space-between':
|
|
102
|
+
if (items.length > 1) {
|
|
103
|
+
extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case 'space-around':
|
|
107
|
+
if (items.length > 0) {
|
|
108
|
+
const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
|
|
109
|
+
const segment = totalSpace / items.length;
|
|
110
|
+
mainOffset = segment / 2;
|
|
111
|
+
extraGap = segment - gap;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// Position each item
|
|
116
|
+
let pos = mainOffset;
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
const mainDim = isRow ? item.w : item.h;
|
|
119
|
+
const crossDim = isRow ? item.h : item.w;
|
|
120
|
+
// Cross-axis alignment
|
|
121
|
+
let crossPos = crossOffset;
|
|
122
|
+
switch (align) {
|
|
123
|
+
case 'start':
|
|
124
|
+
break;
|
|
125
|
+
case 'center':
|
|
126
|
+
crossPos += (crossSize - crossDim) / 2;
|
|
127
|
+
break;
|
|
128
|
+
case 'end':
|
|
129
|
+
crossPos += crossSize - crossDim;
|
|
130
|
+
break;
|
|
131
|
+
case 'stretch':
|
|
132
|
+
if (isRow) {
|
|
133
|
+
item.child.height = crossSize;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
item.child.width = crossSize;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
// Compensate for local bounds offset (e.g. centered anchors)
|
|
141
|
+
if (isRow) {
|
|
142
|
+
item.child.x = pos - item.ox;
|
|
143
|
+
item.child.y = crossPos - item.oy;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
item.child.x = crossPos - item.ox;
|
|
147
|
+
item.child.y = pos - item.oy;
|
|
148
|
+
}
|
|
149
|
+
pos += mainDim + gap + extraGap;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// ─── FlexContainer ───────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Lightweight flexbox-like layout container for PixiJS.
|
|
155
|
+
*
|
|
156
|
+
* Supports row/column direction, justify/align, gap, padding, wrapping,
|
|
157
|
+
* and flex-grow distribution. Zero external dependencies.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* const toolbar = new FlexContainer({
|
|
162
|
+
* direction: 'row',
|
|
163
|
+
* justifyContent: 'space-between',
|
|
164
|
+
* alignItems: 'center',
|
|
165
|
+
* gap: 16,
|
|
166
|
+
* padding: 12,
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* toolbar.addFlexChild(button1);
|
|
170
|
+
* toolbar.addFlexChild(button2);
|
|
171
|
+
* toolbar.resize(800, 60);
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
class FlexContainer extends pixi_js.Container {
|
|
175
|
+
__uiComponent = true;
|
|
176
|
+
_config;
|
|
177
|
+
_padding;
|
|
178
|
+
_maxWidth;
|
|
179
|
+
_maxHeight;
|
|
180
|
+
/** @internal */ _explicitWidth;
|
|
181
|
+
/** @internal */ _explicitHeight;
|
|
182
|
+
_layoutChildren = [];
|
|
183
|
+
_layoutDirty = true;
|
|
184
|
+
constructor(config = {}) {
|
|
185
|
+
super();
|
|
186
|
+
this._config = {
|
|
187
|
+
direction: config.direction ?? 'row',
|
|
188
|
+
justifyContent: config.justifyContent ?? 'start',
|
|
189
|
+
alignItems: config.alignItems ?? 'start',
|
|
190
|
+
gap: config.gap ?? 0,
|
|
191
|
+
flexWrap: config.flexWrap ?? false,
|
|
192
|
+
};
|
|
193
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
194
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
195
|
+
this._maxHeight = config.maxHeight ?? Infinity;
|
|
196
|
+
this._explicitWidth = config.width ?? 0;
|
|
197
|
+
this._explicitHeight = config.height ?? 0;
|
|
198
|
+
}
|
|
199
|
+
// ─── Public API ──────────────────────────────────────
|
|
200
|
+
/** Add a child with optional flex config. Also registers in flex layout. */
|
|
201
|
+
addFlexChild(child, flexConfig) {
|
|
202
|
+
if (flexConfig)
|
|
203
|
+
child._flexConfig = flexConfig;
|
|
204
|
+
if (!this._layoutChildren.includes(child)) {
|
|
205
|
+
this._layoutChildren.push(child);
|
|
206
|
+
this._layoutDirty = true;
|
|
207
|
+
}
|
|
208
|
+
super.addChild(child);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
/** Remove a child from flex layout and display list */
|
|
212
|
+
removeFlexChild(child) {
|
|
213
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
214
|
+
if (idx !== -1) {
|
|
215
|
+
this._layoutChildren.splice(idx, 1);
|
|
216
|
+
this._layoutDirty = true;
|
|
217
|
+
}
|
|
218
|
+
super.removeChild(child);
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
/** Remove all flex children */
|
|
222
|
+
clearFlexChildren() {
|
|
223
|
+
for (const child of this._layoutChildren) {
|
|
224
|
+
super.removeChild(child);
|
|
225
|
+
}
|
|
226
|
+
this._layoutChildren.length = 0;
|
|
227
|
+
this._layoutDirty = true;
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Override addChild so children automatically participate in flex layout.
|
|
232
|
+
* This enables declarative usage from React JSX.
|
|
233
|
+
*/
|
|
234
|
+
addChild(...children) {
|
|
235
|
+
for (const child of children) {
|
|
236
|
+
if (!this._layoutChildren.includes(child)) {
|
|
237
|
+
this._layoutChildren.push(child);
|
|
238
|
+
this._layoutDirty = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const result = super.addChild(...children);
|
|
242
|
+
if (this._layoutDirty)
|
|
243
|
+
this.updateLayout();
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
removeChild(...children) {
|
|
247
|
+
for (const child of children) {
|
|
248
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
249
|
+
if (idx !== -1) {
|
|
250
|
+
this._layoutChildren.splice(idx, 1);
|
|
251
|
+
this._layoutDirty = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return super.removeChild(...children);
|
|
255
|
+
}
|
|
256
|
+
/** Get all flex layout children (read-only) */
|
|
257
|
+
get flexChildren() {
|
|
258
|
+
return this._layoutChildren;
|
|
259
|
+
}
|
|
260
|
+
/** Update the container size and recalculate layout */
|
|
261
|
+
resize(width, height) {
|
|
262
|
+
this._explicitWidth = width;
|
|
263
|
+
this._explicitHeight = height;
|
|
264
|
+
this._layoutDirty = true;
|
|
265
|
+
this.updateLayout();
|
|
266
|
+
}
|
|
267
|
+
/** Update layout direction */
|
|
268
|
+
setDirection(direction) {
|
|
269
|
+
this._config.direction = direction;
|
|
270
|
+
this._layoutDirty = true;
|
|
271
|
+
}
|
|
272
|
+
/** Update justifyContent */
|
|
273
|
+
setJustifyContent(justify) {
|
|
274
|
+
this._config.justifyContent = justify;
|
|
275
|
+
this._layoutDirty = true;
|
|
276
|
+
}
|
|
277
|
+
/** Update alignItems */
|
|
278
|
+
setAlignItems(align) {
|
|
279
|
+
this._config.alignItems = align;
|
|
280
|
+
this._layoutDirty = true;
|
|
281
|
+
}
|
|
282
|
+
/** Update gap */
|
|
283
|
+
setGap(gap) {
|
|
284
|
+
this._config.gap = gap;
|
|
285
|
+
this._layoutDirty = true;
|
|
286
|
+
}
|
|
287
|
+
/** Update padding */
|
|
288
|
+
setPadding(padding) {
|
|
289
|
+
this._padding = normalizePadding(padding);
|
|
290
|
+
this._layoutDirty = true;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Recalculate and apply layout positions for all children.
|
|
294
|
+
* Called automatically by `resize()`. Call manually after
|
|
295
|
+
* adding/removing children without resize.
|
|
296
|
+
*/
|
|
297
|
+
updateLayout() {
|
|
298
|
+
this._layoutDirty = false;
|
|
299
|
+
const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
|
|
300
|
+
const [pt, pr, pb, pl] = this._padding;
|
|
301
|
+
const isRow = direction === 'row';
|
|
302
|
+
const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
|
|
303
|
+
const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
|
|
304
|
+
const mainLimit = isRow ? contentW : contentH;
|
|
305
|
+
const crossLimit = isRow ? contentH : contentW;
|
|
306
|
+
// Measure children
|
|
307
|
+
const measured = this._layoutChildren.map((child) => {
|
|
308
|
+
const { w, h, ox, oy } = measureChild(child);
|
|
309
|
+
return { child, w, h, ox, oy };
|
|
310
|
+
});
|
|
311
|
+
// Split into lines (if wrapping)
|
|
312
|
+
const lines = [];
|
|
313
|
+
if (flexWrap && mainLimit < Infinity) {
|
|
314
|
+
let currentLine = [];
|
|
315
|
+
let lineMain = 0;
|
|
316
|
+
for (const item of measured) {
|
|
317
|
+
const itemMain = isRow ? item.w : item.h;
|
|
318
|
+
const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
|
|
319
|
+
if (currentLine.length > 0 && wouldBe > mainLimit) {
|
|
320
|
+
lines.push(currentLine);
|
|
321
|
+
currentLine = [item];
|
|
322
|
+
lineMain = itemMain;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
currentLine.push(item);
|
|
326
|
+
lineMain = wouldBe;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (currentLine.length > 0)
|
|
330
|
+
lines.push(currentLine);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
lines.push(measured);
|
|
334
|
+
}
|
|
335
|
+
// Compute cross size per line
|
|
336
|
+
const lineCrossSizes = lines.map((line) => {
|
|
337
|
+
let maxCross = 0;
|
|
338
|
+
for (const item of line) {
|
|
339
|
+
const cross = isRow ? item.h : item.w;
|
|
340
|
+
if (cross > maxCross)
|
|
341
|
+
maxCross = cross;
|
|
342
|
+
}
|
|
343
|
+
return maxCross;
|
|
344
|
+
});
|
|
345
|
+
// Layout each line
|
|
346
|
+
let crossOffset = isRow ? pt : pl;
|
|
347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
348
|
+
const line = lines[i];
|
|
349
|
+
const lineCross = lineCrossSizes[i];
|
|
350
|
+
const mainStart = isRow ? pl : pt;
|
|
351
|
+
// Offset items by padding
|
|
352
|
+
const tempItems = line.map((item) => ({ ...item }));
|
|
353
|
+
layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
354
|
+
// Apply main-axis padding offset
|
|
355
|
+
for (const item of tempItems) {
|
|
356
|
+
const origChild = line.find((l) => l.child === item.child);
|
|
357
|
+
origChild.child.x = item.child.x + (isRow ? mainStart : 0);
|
|
358
|
+
origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
|
|
359
|
+
}
|
|
360
|
+
crossOffset += lineCross + gap;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Computed content size (after layout) */
|
|
364
|
+
getContentSize() {
|
|
365
|
+
if (this._layoutDirty)
|
|
366
|
+
this.updateLayout();
|
|
367
|
+
let maxX = 0;
|
|
368
|
+
let maxY = 0;
|
|
369
|
+
for (const child of this._layoutChildren) {
|
|
370
|
+
const { w, h } = measureChild(child);
|
|
371
|
+
maxX = Math.max(maxX, child.x + w);
|
|
372
|
+
maxY = Math.max(maxY, child.y + h);
|
|
373
|
+
}
|
|
374
|
+
const [, pr, pb] = this._padding;
|
|
375
|
+
return { width: maxX + pr, height: maxY + pb };
|
|
376
|
+
}
|
|
377
|
+
/** React reconciler update hook — applies changed config props */
|
|
378
|
+
updateConfig(changed) {
|
|
379
|
+
if ('direction' in changed)
|
|
380
|
+
this.setDirection(changed.direction);
|
|
381
|
+
if ('justifyContent' in changed)
|
|
382
|
+
this.setJustifyContent(changed.justifyContent);
|
|
383
|
+
if ('alignItems' in changed)
|
|
384
|
+
this.setAlignItems(changed.alignItems);
|
|
385
|
+
if ('gap' in changed)
|
|
386
|
+
this.setGap(changed.gap);
|
|
387
|
+
if ('padding' in changed)
|
|
388
|
+
this.setPadding(changed.padding);
|
|
389
|
+
if ('flexWrap' in changed) {
|
|
390
|
+
this._config.flexWrap = changed.flexWrap;
|
|
391
|
+
this._layoutDirty = true;
|
|
392
|
+
}
|
|
393
|
+
if ('width' in changed || 'height' in changed) {
|
|
394
|
+
this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
|
|
395
|
+
return; // resize calls updateLayout
|
|
396
|
+
}
|
|
397
|
+
if (this._layoutDirty)
|
|
398
|
+
this.updateLayout();
|
|
399
|
+
}
|
|
400
|
+
destroy(options) {
|
|
401
|
+
this._layoutChildren.length = 0;
|
|
402
|
+
super.destroy(options);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Collection of easing functions for use with Tween and Timeline.
|
|
408
|
+
*
|
|
409
|
+
* All functions take a progress value t (0..1) and return the eased value.
|
|
410
|
+
*/
|
|
411
|
+
const Easing = {
|
|
412
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
413
|
+
easeInCubic: (t) => t * t * t,
|
|
414
|
+
easeOutCubic: (t) => --t * t * t + 1,
|
|
415
|
+
easeOutBack: (t) => {
|
|
416
|
+
const c1 = 1.70158;
|
|
417
|
+
const c3 = c1 + 1;
|
|
418
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
419
|
+
}};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Lightweight tween system integrated with PixiJS Ticker.
|
|
423
|
+
* Zero external dependencies — no GSAP required.
|
|
424
|
+
*
|
|
425
|
+
* All tweens return a Promise that resolves on completion.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```ts
|
|
429
|
+
* // Fade in a sprite
|
|
430
|
+
* await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
|
|
431
|
+
*
|
|
432
|
+
* // Move and wait
|
|
433
|
+
* await Tween.to(sprite, { x: 500 }, 300);
|
|
434
|
+
*
|
|
435
|
+
* // From a starting value
|
|
436
|
+
* await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
class Tween {
|
|
440
|
+
static _tweens = [];
|
|
441
|
+
static _tickerAdded = false;
|
|
442
|
+
/**
|
|
443
|
+
* Animate properties from current values to target values.
|
|
444
|
+
*
|
|
445
|
+
* @param target - Object to animate (Sprite, Container, etc.)
|
|
446
|
+
* @param props - Target property values
|
|
447
|
+
* @param duration - Duration in milliseconds
|
|
448
|
+
* @param easing - Easing function (default: easeOutQuad)
|
|
449
|
+
* @param onUpdate - Progress callback (0..1)
|
|
450
|
+
*/
|
|
451
|
+
static to(target, props, duration, easing, onUpdate) {
|
|
452
|
+
return new Promise((resolve) => {
|
|
453
|
+
// Capture starting values
|
|
454
|
+
const from = {};
|
|
455
|
+
for (const key of Object.keys(props)) {
|
|
456
|
+
from[key] = Tween.getProperty(target, key);
|
|
457
|
+
}
|
|
458
|
+
const tween = {
|
|
459
|
+
target,
|
|
460
|
+
from,
|
|
461
|
+
to: { ...props },
|
|
462
|
+
duration: Math.max(1, duration),
|
|
463
|
+
easing: easing ?? Easing.easeOutQuad,
|
|
464
|
+
elapsed: 0,
|
|
465
|
+
delay: 0,
|
|
466
|
+
resolve,
|
|
467
|
+
onUpdate,
|
|
468
|
+
};
|
|
469
|
+
Tween._tweens.push(tween);
|
|
470
|
+
Tween.ensureTicker();
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Animate properties from given values to current values.
|
|
475
|
+
*/
|
|
476
|
+
static from(target, props, duration, easing, onUpdate) {
|
|
477
|
+
// Capture current values as "to"
|
|
478
|
+
const to = {};
|
|
479
|
+
for (const key of Object.keys(props)) {
|
|
480
|
+
to[key] = Tween.getProperty(target, key);
|
|
481
|
+
Tween.setProperty(target, key, props[key]);
|
|
482
|
+
}
|
|
483
|
+
return Tween.to(target, to, duration, easing, onUpdate);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Animate from one set of values to another.
|
|
487
|
+
*/
|
|
488
|
+
static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
|
|
489
|
+
// Set starting values
|
|
490
|
+
for (const key of Object.keys(fromProps)) {
|
|
491
|
+
Tween.setProperty(target, key, fromProps[key]);
|
|
492
|
+
}
|
|
493
|
+
return Tween.to(target, toProps, duration, easing, onUpdate);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Wait for a given duration (useful in timelines).
|
|
497
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
498
|
+
*/
|
|
499
|
+
static delay(ms) {
|
|
500
|
+
return new Promise((resolve) => {
|
|
501
|
+
let elapsed = 0;
|
|
502
|
+
const onTick = (ticker) => {
|
|
503
|
+
elapsed += ticker.deltaMS;
|
|
504
|
+
if (elapsed >= ms) {
|
|
505
|
+
pixi_js.Ticker.shared.remove(onTick);
|
|
506
|
+
resolve();
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
pixi_js.Ticker.shared.add(onTick);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Kill all tweens on a target.
|
|
514
|
+
*/
|
|
515
|
+
static killTweensOf(target) {
|
|
516
|
+
Tween._tweens = Tween._tweens.filter((tw) => {
|
|
517
|
+
if (tw.target === target) {
|
|
518
|
+
tw.resolve();
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
return true;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Kill all active tweens.
|
|
526
|
+
*/
|
|
527
|
+
static killAll() {
|
|
528
|
+
for (const tw of Tween._tweens) {
|
|
529
|
+
tw.resolve();
|
|
530
|
+
}
|
|
531
|
+
Tween._tweens.length = 0;
|
|
532
|
+
}
|
|
533
|
+
/** Number of active tweens */
|
|
534
|
+
static get activeTweens() {
|
|
535
|
+
return Tween._tweens.length;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
539
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
540
|
+
*/
|
|
541
|
+
static reset() {
|
|
542
|
+
for (const tw of Tween._tweens) {
|
|
543
|
+
tw.resolve();
|
|
544
|
+
}
|
|
545
|
+
Tween._tweens.length = 0;
|
|
546
|
+
if (Tween._tickerAdded) {
|
|
547
|
+
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
548
|
+
Tween._tickerAdded = false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// ─── Internal ──────────────────────────────────────────
|
|
552
|
+
static ensureTicker() {
|
|
553
|
+
if (Tween._tickerAdded)
|
|
554
|
+
return;
|
|
555
|
+
Tween._tickerAdded = true;
|
|
556
|
+
pixi_js.Ticker.shared.add(Tween.tick);
|
|
557
|
+
}
|
|
558
|
+
static tick = (ticker) => {
|
|
559
|
+
const dt = ticker.deltaMS;
|
|
560
|
+
const completed = [];
|
|
561
|
+
for (const tw of Tween._tweens) {
|
|
562
|
+
tw.elapsed += dt;
|
|
563
|
+
if (tw.elapsed < tw.delay)
|
|
564
|
+
continue;
|
|
565
|
+
const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
|
|
566
|
+
const t = tw.easing(raw);
|
|
567
|
+
// Interpolate each property
|
|
568
|
+
for (const key of Object.keys(tw.to)) {
|
|
569
|
+
const start = tw.from[key];
|
|
570
|
+
const end = tw.to[key];
|
|
571
|
+
const value = start + (end - start) * t;
|
|
572
|
+
Tween.setProperty(tw.target, key, value);
|
|
573
|
+
}
|
|
574
|
+
tw.onUpdate?.(raw);
|
|
575
|
+
if (raw >= 1) {
|
|
576
|
+
completed.push(tw);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// Remove completed tweens
|
|
580
|
+
for (const tw of completed) {
|
|
581
|
+
const idx = Tween._tweens.indexOf(tw);
|
|
582
|
+
if (idx !== -1)
|
|
583
|
+
Tween._tweens.splice(idx, 1);
|
|
584
|
+
tw.resolve();
|
|
585
|
+
}
|
|
586
|
+
// Remove ticker when no active tweens
|
|
587
|
+
if (Tween._tweens.length === 0 && Tween._tickerAdded) {
|
|
588
|
+
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
589
|
+
Tween._tickerAdded = false;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
|
|
594
|
+
*/
|
|
595
|
+
static getProperty(target, key) {
|
|
596
|
+
const parts = key.split('.');
|
|
597
|
+
let obj = target;
|
|
598
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
599
|
+
obj = obj[parts[i]];
|
|
600
|
+
}
|
|
601
|
+
return obj[parts[parts.length - 1]] ?? 0;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Set a potentially nested property.
|
|
605
|
+
*/
|
|
606
|
+
static setProperty(target, key, value) {
|
|
607
|
+
const parts = key.split('.');
|
|
608
|
+
let obj = target;
|
|
609
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
610
|
+
obj = obj[parts[i]];
|
|
611
|
+
}
|
|
612
|
+
obj[parts[parts.length - 1]] = value;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
7
615
|
|
|
8
616
|
const DEFAULT_COLORS = {
|
|
9
617
|
default: 0xffd700,
|
|
@@ -13,33 +621,55 @@ const DEFAULT_COLORS = {
|
|
|
13
621
|
};
|
|
14
622
|
function makeGraphicsView(w, h, radius, color) {
|
|
15
623
|
const g = new pixi_js.Graphics();
|
|
16
|
-
g.roundRect(
|
|
17
|
-
// Highlight overlay
|
|
18
|
-
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
624
|
+
g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
|
|
19
625
|
return g;
|
|
20
626
|
}
|
|
21
627
|
/**
|
|
22
|
-
* Interactive button
|
|
628
|
+
* Interactive button with per-state custom views and animations.
|
|
23
629
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
630
|
+
* Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
|
|
631
|
+
* (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
|
|
632
|
+
* Falls back to colored Graphics when no custom view is provided.
|
|
26
633
|
*
|
|
27
634
|
* @example
|
|
28
635
|
* ```ts
|
|
636
|
+
* // Graphics-based (quick prototyping)
|
|
29
637
|
* const btn = new Button({
|
|
30
638
|
* width: 200, height: 60, borderRadius: 12,
|
|
31
639
|
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
32
640
|
* text: 'SPIN',
|
|
641
|
+
* onPress: () => spin(),
|
|
642
|
+
* });
|
|
643
|
+
*
|
|
644
|
+
* // Asset-based (production art)
|
|
645
|
+
* const btn = new Button({
|
|
646
|
+
* defaultView: 'btn-idle',
|
|
647
|
+
* hoverView: 'btn-hover',
|
|
648
|
+
* pressedView: 'btn-pressed',
|
|
649
|
+
* disabledView: 'btn-disabled',
|
|
650
|
+
* text: 'SPIN',
|
|
651
|
+
* onPress: () => spin(),
|
|
33
652
|
* });
|
|
34
653
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
654
|
+
* // Custom Container view
|
|
655
|
+
* const btn = new Button({
|
|
656
|
+
* defaultView: myAnimatedSprite,
|
|
657
|
+
* text: 'SPIN',
|
|
658
|
+
* });
|
|
37
659
|
* ```
|
|
38
660
|
*/
|
|
39
|
-
class Button extends
|
|
40
|
-
|
|
661
|
+
class Button extends pixi_js.Container {
|
|
662
|
+
__uiComponent = true;
|
|
663
|
+
_views = new Map();
|
|
664
|
+
_state = 'default';
|
|
665
|
+
_enabled = true;
|
|
666
|
+
_config;
|
|
667
|
+
_textObj = null;
|
|
668
|
+
/** Press callback */
|
|
669
|
+
onPress;
|
|
41
670
|
constructor(config = {}) {
|
|
42
|
-
|
|
671
|
+
super();
|
|
672
|
+
this._config = {
|
|
43
673
|
width: config.width ?? 200,
|
|
44
674
|
height: config.height ?? 60,
|
|
45
675
|
borderRadius: config.borderRadius ?? 8,
|
|
@@ -47,81 +677,202 @@ class Button extends ui.FancyButton {
|
|
|
47
677
|
animationDuration: config.animationDuration ?? 100,
|
|
48
678
|
...config,
|
|
49
679
|
};
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
67
|
-
if (config.textures) {
|
|
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
|
-
}
|
|
680
|
+
this.onPress = config.onPress;
|
|
681
|
+
this._buildViews(config);
|
|
84
682
|
// Text
|
|
85
683
|
if (config.text) {
|
|
86
|
-
|
|
684
|
+
this._textObj = new pixi_js.Text({
|
|
685
|
+
text: config.text,
|
|
686
|
+
style: {
|
|
687
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
688
|
+
fontSize: 20,
|
|
689
|
+
fill: 0xffffff,
|
|
690
|
+
fontWeight: 'bold',
|
|
691
|
+
...config.textStyle,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
this._textObj.anchor.set(0.5);
|
|
695
|
+
this.addChild(this._textObj);
|
|
87
696
|
}
|
|
88
|
-
|
|
89
|
-
this.
|
|
697
|
+
// Interaction
|
|
698
|
+
this.eventMode = 'static';
|
|
699
|
+
this.cursor = 'pointer';
|
|
700
|
+
this.on('pointerover', this._onPointerOver, this);
|
|
701
|
+
this.on('pointerout', this._onPointerOut, this);
|
|
702
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
703
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
704
|
+
this.on('pointerupoutside', this._onPointerUpOutside, this);
|
|
90
705
|
if (config.disabled) {
|
|
91
706
|
this.enabled = false;
|
|
92
707
|
}
|
|
93
708
|
}
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
this.
|
|
709
|
+
/** Current button state */
|
|
710
|
+
get state() {
|
|
711
|
+
return this._state;
|
|
712
|
+
}
|
|
713
|
+
/** Enable the button */
|
|
714
|
+
enable() {
|
|
715
|
+
this.enabled = true;
|
|
716
|
+
}
|
|
717
|
+
/** Disable the button */
|
|
718
|
+
disable() {
|
|
719
|
+
this.enabled = false;
|
|
720
|
+
}
|
|
721
|
+
/** Whether the button is enabled */
|
|
722
|
+
get enabled() {
|
|
723
|
+
return this._enabled;
|
|
724
|
+
}
|
|
725
|
+
set enabled(value) {
|
|
726
|
+
this._enabled = value;
|
|
727
|
+
this.cursor = value ? 'pointer' : 'default';
|
|
728
|
+
this.eventMode = value ? 'static' : 'none';
|
|
729
|
+
this._setState(value ? 'default' : 'disabled');
|
|
730
|
+
}
|
|
731
|
+
/** Whether the button is disabled */
|
|
732
|
+
get disabled() {
|
|
733
|
+
return !this._enabled;
|
|
734
|
+
}
|
|
735
|
+
/** Update button text */
|
|
736
|
+
set text(value) {
|
|
737
|
+
if (this._textObj) {
|
|
738
|
+
this._textObj.text = value;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// ─── View building ──────────────────────────────────
|
|
742
|
+
_buildViews(config) {
|
|
743
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
744
|
+
const { width, height, borderRadius } = this._config;
|
|
745
|
+
const stateViews = {
|
|
746
|
+
default: config.defaultView,
|
|
747
|
+
hover: config.hoverView,
|
|
748
|
+
pressed: config.pressedView,
|
|
749
|
+
disabled: config.disabledView,
|
|
750
|
+
};
|
|
751
|
+
const states = ['default', 'hover', 'pressed', 'disabled'];
|
|
752
|
+
for (const state of states) {
|
|
753
|
+
const customView = resolveView(stateViews[state]);
|
|
754
|
+
const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
|
|
755
|
+
view.visible = state === 'default';
|
|
756
|
+
this._views.set(state, view);
|
|
757
|
+
this.addChild(view);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
_rebuildViews() {
|
|
761
|
+
for (const [, view] of this._views) {
|
|
762
|
+
this.removeChild(view);
|
|
763
|
+
view.destroy();
|
|
764
|
+
}
|
|
765
|
+
this._views.clear();
|
|
766
|
+
this._buildViews(this._config);
|
|
767
|
+
// Re-insert views before text
|
|
768
|
+
if (this._textObj && this._textObj.parent === this) {
|
|
769
|
+
this.setChildIndex(this._textObj, this.children.length - 1);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// ─── State management ───────────────────────────────
|
|
773
|
+
_setState(state) {
|
|
774
|
+
if (this._state === state)
|
|
775
|
+
return;
|
|
776
|
+
this._state = state;
|
|
777
|
+
for (const [s, view] of this._views) {
|
|
778
|
+
view.visible = s === state;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
_onPointerOver() {
|
|
782
|
+
if (!this._enabled)
|
|
783
|
+
return;
|
|
784
|
+
this._setState('hover');
|
|
785
|
+
Tween.killTweensOf(this);
|
|
786
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
787
|
+
}
|
|
788
|
+
_onPointerOut() {
|
|
789
|
+
if (!this._enabled)
|
|
790
|
+
return;
|
|
791
|
+
this._setState('default');
|
|
792
|
+
Tween.killTweensOf(this);
|
|
793
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
794
|
+
}
|
|
795
|
+
_onPointerDown() {
|
|
796
|
+
if (!this._enabled)
|
|
797
|
+
return;
|
|
798
|
+
this._setState('pressed');
|
|
799
|
+
Tween.killTweensOf(this);
|
|
800
|
+
const s = this._config.pressScale;
|
|
801
|
+
Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
97
802
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
803
|
+
_onPointerUp() {
|
|
804
|
+
if (!this._enabled)
|
|
805
|
+
return;
|
|
806
|
+
this._setState('hover');
|
|
807
|
+
Tween.killTweensOf(this);
|
|
808
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
809
|
+
this.onPress?.();
|
|
101
810
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
811
|
+
_onPointerUpOutside() {
|
|
812
|
+
if (!this._enabled)
|
|
813
|
+
return;
|
|
814
|
+
this._setState('default');
|
|
815
|
+
Tween.killTweensOf(this);
|
|
816
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
817
|
+
}
|
|
818
|
+
/** React reconciler update hook */
|
|
819
|
+
updateConfig(changed) {
|
|
820
|
+
if ('text' in changed && this._textObj)
|
|
821
|
+
this._textObj.text = changed.text;
|
|
822
|
+
if ('disabled' in changed)
|
|
823
|
+
this.enabled = !changed.disabled;
|
|
824
|
+
if ('onPress' in changed)
|
|
825
|
+
this.onPress = changed.onPress;
|
|
826
|
+
const structural = [
|
|
827
|
+
'colors', 'width', 'height', 'borderRadius', 'textStyle',
|
|
828
|
+
'defaultView', 'hoverView', 'pressedView', 'disabledView',
|
|
829
|
+
];
|
|
830
|
+
const needsRebuild = structural.some((k) => k in changed);
|
|
831
|
+
if (needsRebuild) {
|
|
832
|
+
Object.assign(this._config, changed);
|
|
833
|
+
this._rebuildViews();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
destroy(options) {
|
|
837
|
+
Tween.killTweensOf(this);
|
|
838
|
+
this.off('pointerover', this._onPointerOver, this);
|
|
839
|
+
this.off('pointerout', this._onPointerOut, this);
|
|
840
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
841
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
842
|
+
this.off('pointerupoutside', this._onPointerUpOutside, this);
|
|
843
|
+
this._views.clear();
|
|
844
|
+
this._textObj = null;
|
|
845
|
+
super.destroy(options);
|
|
105
846
|
}
|
|
106
847
|
}
|
|
107
848
|
|
|
108
|
-
function makeBarGraphics(w, h, radius, color) {
|
|
109
|
-
return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
110
|
-
}
|
|
111
849
|
/**
|
|
112
|
-
* Horizontal progress bar
|
|
850
|
+
* Horizontal progress bar with optional custom track/fill views.
|
|
113
851
|
*
|
|
114
|
-
*
|
|
852
|
+
* Supports asset-based skinning: provide `trackView` and/or `fillView`
|
|
853
|
+
* as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
|
|
854
|
+
* Falls back to colored Graphics when no custom views are provided.
|
|
115
855
|
*
|
|
116
856
|
* @example
|
|
117
857
|
* ```ts
|
|
858
|
+
* // Graphics-based (quick prototyping)
|
|
118
859
|
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
119
|
-
*
|
|
120
|
-
*
|
|
860
|
+
* bar.progress = 0.5;
|
|
861
|
+
*
|
|
862
|
+
* // Asset-based (production art)
|
|
863
|
+
* const bar = new ProgressBar({
|
|
864
|
+
* width: 300, height: 20,
|
|
865
|
+
* trackView: 'bar-track',
|
|
866
|
+
* fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
|
|
867
|
+
* });
|
|
868
|
+
* bar.progress = 0.75;
|
|
121
869
|
* ```
|
|
122
870
|
*/
|
|
123
871
|
class ProgressBar extends pixi_js.Container {
|
|
124
|
-
|
|
872
|
+
__uiComponent = true;
|
|
873
|
+
_track;
|
|
874
|
+
_fill;
|
|
875
|
+
_fillMask;
|
|
125
876
|
_borderGfx;
|
|
126
877
|
_config;
|
|
127
878
|
_progress = 0;
|
|
@@ -140,21 +891,39 @@ class ProgressBar extends pixi_js.Container {
|
|
|
140
891
|
animationSpeed: config.animationSpeed ?? 0.1,
|
|
141
892
|
};
|
|
142
893
|
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
894
|
+
// Track background — custom view or Graphics
|
|
895
|
+
const customTrack = resolveView(config.trackView);
|
|
896
|
+
if (customTrack) {
|
|
897
|
+
customTrack.width = width;
|
|
898
|
+
customTrack.height = height;
|
|
899
|
+
this._track = customTrack;
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
const g = new pixi_js.Graphics();
|
|
903
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
904
|
+
this._track = g;
|
|
905
|
+
}
|
|
906
|
+
this.addChild(this._track);
|
|
907
|
+
// Fill bar — custom view or Graphics
|
|
908
|
+
const customFill = resolveView(config.fillView);
|
|
909
|
+
if (customFill) {
|
|
910
|
+
customFill.x = borderWidth;
|
|
911
|
+
customFill.y = borderWidth;
|
|
912
|
+
customFill.width = width - borderWidth * 2;
|
|
913
|
+
customFill.height = height - borderWidth * 2;
|
|
914
|
+
this._fill = customFill;
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
const g = new pixi_js.Graphics();
|
|
918
|
+
g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
|
|
919
|
+
this._fill = g;
|
|
920
|
+
}
|
|
921
|
+
this.addChild(this._fill);
|
|
922
|
+
// Mask for the fill (controls visible width)
|
|
923
|
+
this._fillMask = new pixi_js.Graphics();
|
|
924
|
+
this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
|
|
925
|
+
this.addChild(this._fillMask);
|
|
926
|
+
this._fill.mask = this._fillMask;
|
|
158
927
|
// Border overlay
|
|
159
928
|
this._borderGfx = new pixi_js.Graphics();
|
|
160
929
|
if (borderColor !== undefined && borderWidth > 0) {
|
|
@@ -172,7 +941,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
172
941
|
this._progress = Math.max(0, Math.min(1, value));
|
|
173
942
|
if (!this._config.animated) {
|
|
174
943
|
this._displayedProgress = this._progress;
|
|
175
|
-
this.
|
|
944
|
+
this.updateMask();
|
|
176
945
|
}
|
|
177
946
|
}
|
|
178
947
|
/**
|
|
@@ -183,11 +952,26 @@ class ProgressBar extends pixi_js.Container {
|
|
|
183
952
|
return;
|
|
184
953
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
185
954
|
this._displayedProgress = this._progress;
|
|
955
|
+
this.updateMask();
|
|
186
956
|
return;
|
|
187
957
|
}
|
|
188
958
|
this._displayedProgress +=
|
|
189
959
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
190
|
-
this.
|
|
960
|
+
this.updateMask();
|
|
961
|
+
}
|
|
962
|
+
/** React reconciler update hook */
|
|
963
|
+
updateConfig(changed) {
|
|
964
|
+
if ('progress' in changed)
|
|
965
|
+
this.progress = changed.progress;
|
|
966
|
+
if ('animated' in changed)
|
|
967
|
+
this._config.animated = changed.animated;
|
|
968
|
+
if ('animationSpeed' in changed)
|
|
969
|
+
this._config.animationSpeed = changed.animationSpeed;
|
|
970
|
+
}
|
|
971
|
+
updateMask() {
|
|
972
|
+
const w = this._config.width * this._displayedProgress;
|
|
973
|
+
this._fillMask.clear();
|
|
974
|
+
this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
|
|
191
975
|
}
|
|
192
976
|
}
|
|
193
977
|
|
|
@@ -205,6 +989,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
205
989
|
* ```
|
|
206
990
|
*/
|
|
207
991
|
class Label extends pixi_js.Container {
|
|
992
|
+
__uiComponent = true;
|
|
208
993
|
_text;
|
|
209
994
|
_maxWidth;
|
|
210
995
|
_autoFit;
|
|
@@ -271,6 +1056,21 @@ class Label extends pixi_js.Container {
|
|
|
271
1056
|
maximumFractionDigits: decimals,
|
|
272
1057
|
}).format(value);
|
|
273
1058
|
}
|
|
1059
|
+
/** React reconciler update hook */
|
|
1060
|
+
updateConfig(changed) {
|
|
1061
|
+
if ('text' in changed)
|
|
1062
|
+
this.text = changed.text;
|
|
1063
|
+
if ('maxWidth' in changed)
|
|
1064
|
+
this.maxWidth = changed.maxWidth;
|
|
1065
|
+
if ('autoFit' in changed) {
|
|
1066
|
+
this._autoFit = changed.autoFit;
|
|
1067
|
+
this.fitText();
|
|
1068
|
+
}
|
|
1069
|
+
if ('style' in changed && typeof changed.style === 'object') {
|
|
1070
|
+
Object.assign(this._text.style, changed.style);
|
|
1071
|
+
this.fitText();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
274
1074
|
fitText() {
|
|
275
1075
|
if (!this._autoFit || this._maxWidth === Infinity)
|
|
276
1076
|
return;
|
|
@@ -283,10 +1083,10 @@ class Label extends pixi_js.Container {
|
|
|
283
1083
|
}
|
|
284
1084
|
|
|
285
1085
|
/**
|
|
286
|
-
* Background panel
|
|
1086
|
+
* Background panel with optional flexbox content layout.
|
|
287
1087
|
*
|
|
288
1088
|
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
289
|
-
* Children added
|
|
1089
|
+
* Children added via `addContent()` participate in flex layout automatically.
|
|
290
1090
|
*
|
|
291
1091
|
* @example
|
|
292
1092
|
* ```ts
|
|
@@ -301,9 +1101,14 @@ class Label extends pixi_js.Container {
|
|
|
301
1101
|
* });
|
|
302
1102
|
* ```
|
|
303
1103
|
*/
|
|
304
|
-
class Panel extends
|
|
1104
|
+
class Panel extends pixi_js.Container {
|
|
1105
|
+
__uiComponent = true;
|
|
1106
|
+
_bg;
|
|
1107
|
+
_content;
|
|
1108
|
+
_internalSetup = true;
|
|
305
1109
|
_panelConfig;
|
|
306
1110
|
constructor(config = {}) {
|
|
1111
|
+
super();
|
|
307
1112
|
const resolvedConfig = {
|
|
308
1113
|
width: config.width ?? 400,
|
|
309
1114
|
height: config.height ?? 300,
|
|
@@ -311,8 +1116,8 @@ class Panel extends components.LayoutContainer {
|
|
|
311
1116
|
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
312
1117
|
...config,
|
|
313
1118
|
};
|
|
314
|
-
|
|
315
|
-
|
|
1119
|
+
this._panelConfig = resolvedConfig;
|
|
1120
|
+
// Create background
|
|
316
1121
|
if (config.nineSliceTexture) {
|
|
317
1122
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
318
1123
|
? pixi_js.Texture.from(config.nineSliceTexture)
|
|
@@ -328,126 +1133,110 @@ class Panel extends components.LayoutContainer {
|
|
|
328
1133
|
nineSlice.width = resolvedConfig.width;
|
|
329
1134
|
nineSlice.height = resolvedConfig.height;
|
|
330
1135
|
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
331
|
-
|
|
1136
|
+
this._bg = nineSlice;
|
|
332
1137
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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;
|
|
1138
|
+
else {
|
|
1139
|
+
const g = new pixi_js.Graphics();
|
|
1140
|
+
const bgColor = config.backgroundColor ?? 0x1a1a2e;
|
|
1141
|
+
const radius = config.borderRadius ?? 0;
|
|
1142
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
|
|
346
1143
|
if (config.borderColor !== undefined && config.borderWidth) {
|
|
347
|
-
|
|
348
|
-
|
|
1144
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
|
|
1145
|
+
.stroke({ color: config.borderColor, width: config.borderWidth });
|
|
349
1146
|
}
|
|
1147
|
+
g.alpha = resolvedConfig.backgroundAlpha;
|
|
1148
|
+
this._bg = g;
|
|
350
1149
|
}
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1150
|
+
this.addChild(this._bg);
|
|
1151
|
+
// Create content flex container
|
|
1152
|
+
this._content = new FlexContainer({
|
|
1153
|
+
...config.layout,
|
|
1154
|
+
direction: config.layout?.direction ?? 'column',
|
|
1155
|
+
justifyContent: config.layout?.justifyContent ?? 'start',
|
|
1156
|
+
alignItems: config.layout?.alignItems ?? 'start',
|
|
1157
|
+
gap: config.layout?.gap ?? 0,
|
|
1158
|
+
padding: resolvedConfig.padding,
|
|
1159
|
+
width: resolvedConfig.width,
|
|
1160
|
+
height: resolvedConfig.height,
|
|
1161
|
+
});
|
|
1162
|
+
this.addChild(this._content);
|
|
1163
|
+
this._internalSetup = false;
|
|
355
1164
|
}
|
|
356
|
-
/** Access the content container
|
|
1165
|
+
/** Access the content flex container — add children here for layout */
|
|
357
1166
|
get content() {
|
|
358
|
-
return this.
|
|
1167
|
+
return this._content;
|
|
1168
|
+
}
|
|
1169
|
+
/** Convenience: add a child to the content layout */
|
|
1170
|
+
addContent(child) {
|
|
1171
|
+
this._content.addFlexChild(child);
|
|
1172
|
+
this._content.updateLayout();
|
|
1173
|
+
return this;
|
|
359
1174
|
}
|
|
360
1175
|
/** Resize the panel */
|
|
361
1176
|
setSize(width, height) {
|
|
362
1177
|
this._panelConfig.width = width;
|
|
363
1178
|
this._panelConfig.height = height;
|
|
364
|
-
|
|
1179
|
+
// Resize background
|
|
1180
|
+
if (this._bg instanceof pixi_js.NineSliceSprite) {
|
|
1181
|
+
this._bg.width = width;
|
|
1182
|
+
this._bg.height = height;
|
|
1183
|
+
}
|
|
1184
|
+
else if (this._bg instanceof pixi_js.Graphics) {
|
|
1185
|
+
const radius = this._panelConfig.borderRadius ?? 0;
|
|
1186
|
+
const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
|
|
1187
|
+
this._bg.clear();
|
|
1188
|
+
this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
|
|
1189
|
+
if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
|
|
1190
|
+
this._bg.roundRect(0, 0, width, height, radius)
|
|
1191
|
+
.stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
|
|
1192
|
+
}
|
|
1193
|
+
this._bg.alpha = this._panelConfig.backgroundAlpha;
|
|
1194
|
+
}
|
|
1195
|
+
this._content.resize(width, height);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Override addChild so external children are routed to content FlexContainer.
|
|
1199
|
+
* Enables `<panel><label /><button /></panel>` in React JSX.
|
|
1200
|
+
*/
|
|
1201
|
+
addChild(...children) {
|
|
1202
|
+
if (this._internalSetup) {
|
|
1203
|
+
return super.addChild(...children);
|
|
1204
|
+
}
|
|
1205
|
+
for (const child of children) {
|
|
1206
|
+
this._content.addFlexChild(child);
|
|
1207
|
+
}
|
|
1208
|
+
this._content.updateLayout();
|
|
1209
|
+
return children[0];
|
|
1210
|
+
}
|
|
1211
|
+
removeChild(...children) {
|
|
1212
|
+
if (this._internalSetup) {
|
|
1213
|
+
return super.removeChild(...children);
|
|
1214
|
+
}
|
|
1215
|
+
for (const child of children) {
|
|
1216
|
+
this._content.removeFlexChild(child);
|
|
1217
|
+
}
|
|
1218
|
+
return children[0];
|
|
1219
|
+
}
|
|
1220
|
+
/** React reconciler update hook */
|
|
1221
|
+
updateConfig(changed) {
|
|
1222
|
+
if ('width' in changed || 'height' in changed) {
|
|
1223
|
+
this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
|
|
1224
|
+
}
|
|
1225
|
+
if ('backgroundAlpha' in changed) {
|
|
1226
|
+
this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
|
|
1227
|
+
this._bg.alpha = changed.backgroundAlpha;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
destroy(options) {
|
|
1231
|
+
super.destroy(options);
|
|
365
1232
|
}
|
|
366
1233
|
}
|
|
367
1234
|
|
|
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
|
-
|
|
446
1235
|
/**
|
|
447
1236
|
* Reactive balance display component.
|
|
448
1237
|
*
|
|
449
1238
|
* Automatically formats currency and can animate value changes
|
|
450
|
-
* with a smooth countup/countdown effect.
|
|
1239
|
+
* with a smooth countup/countdown effect using engine Tween.
|
|
451
1240
|
*
|
|
452
1241
|
* @example
|
|
453
1242
|
* ```ts
|
|
@@ -459,13 +1248,14 @@ const Easing = {
|
|
|
459
1248
|
* ```
|
|
460
1249
|
*/
|
|
461
1250
|
class BalanceDisplay extends pixi_js.Container {
|
|
1251
|
+
__uiComponent = true;
|
|
462
1252
|
_prefixLabel = null;
|
|
463
1253
|
_valueLabel;
|
|
464
1254
|
_config;
|
|
465
1255
|
_currentValue = 0;
|
|
466
1256
|
_displayedValue = 0;
|
|
467
|
-
|
|
468
|
-
|
|
1257
|
+
/** Internal target for Tween animation */
|
|
1258
|
+
_tweenTarget = { value: 0 };
|
|
469
1259
|
constructor(config = {}) {
|
|
470
1260
|
super();
|
|
471
1261
|
this._config = {
|
|
@@ -526,37 +1316,13 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
526
1316
|
this._config.currency = currency;
|
|
527
1317
|
this.updateDisplay();
|
|
528
1318
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const startTime = Date.now();
|
|
537
|
-
return new Promise((resolve) => {
|
|
538
|
-
const tick = () => {
|
|
539
|
-
if (this._animationCancelled) {
|
|
540
|
-
this._animating = false;
|
|
541
|
-
resolve();
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const elapsed = Date.now() - startTime;
|
|
545
|
-
const t = Math.min(elapsed / duration, 1);
|
|
546
|
-
const eased = Easing.easeOutCubic(t);
|
|
547
|
-
this._displayedValue = from + (to - from) * eased;
|
|
548
|
-
this.updateDisplay();
|
|
549
|
-
if (t < 1) {
|
|
550
|
-
requestAnimationFrame(tick);
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
this._displayedValue = to;
|
|
554
|
-
this.updateDisplay();
|
|
555
|
-
this._animating = false;
|
|
556
|
-
resolve();
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
requestAnimationFrame(tick);
|
|
1319
|
+
animateValue(from, to) {
|
|
1320
|
+
// Cancel any running animation
|
|
1321
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1322
|
+
this._tweenTarget.value = from;
|
|
1323
|
+
Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
|
|
1324
|
+
this._displayedValue = this._tweenTarget.value;
|
|
1325
|
+
this.updateDisplay();
|
|
560
1326
|
});
|
|
561
1327
|
}
|
|
562
1328
|
updateDisplay() {
|
|
@@ -566,305 +1332,120 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
566
1332
|
if (this._prefixLabel) {
|
|
567
1333
|
this._prefixLabel.y = -14;
|
|
568
1334
|
this._valueLabel.y = 14;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
*
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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.
|
|
753
|
-
*/
|
|
754
|
-
static delay(ms) {
|
|
755
|
-
return new Promise((resolve) => {
|
|
756
|
-
let elapsed = 0;
|
|
757
|
-
const onTick = (ticker) => {
|
|
758
|
-
elapsed += ticker.deltaMS;
|
|
759
|
-
if (elapsed >= ms) {
|
|
760
|
-
pixi_js.Ticker.shared.remove(onTick);
|
|
761
|
-
resolve();
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
pixi_js.Ticker.shared.add(onTick);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
/** React reconciler update hook */
|
|
1338
|
+
updateConfig(changed) {
|
|
1339
|
+
if ('value' in changed)
|
|
1340
|
+
this.setValue(changed.value);
|
|
1341
|
+
if ('currency' in changed)
|
|
1342
|
+
this.setCurrency(changed.currency);
|
|
1343
|
+
}
|
|
1344
|
+
destroy(options) {
|
|
1345
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1346
|
+
super.destroy(options);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Win amount display with countup animation.
|
|
1352
|
+
*
|
|
1353
|
+
* Shows a dramatic countup from 0 to the win amount, with optional
|
|
1354
|
+
* scale pop effect — typical of slot games. Uses engine Tween system.
|
|
1355
|
+
*
|
|
1356
|
+
* @example
|
|
1357
|
+
* ```ts
|
|
1358
|
+
* const winDisplay = new WinDisplay({ currency: 'USD' });
|
|
1359
|
+
* scene.container.addChild(winDisplay);
|
|
1360
|
+
* await winDisplay.showWin(150.50); // countup animation
|
|
1361
|
+
* winDisplay.hide();
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
class WinDisplay extends pixi_js.Container {
|
|
1365
|
+
__uiComponent = true;
|
|
1366
|
+
_label;
|
|
1367
|
+
_config;
|
|
1368
|
+
/** Internal target for Tween countup */
|
|
1369
|
+
_tweenTarget = { value: 0 };
|
|
1370
|
+
constructor(config = {}) {
|
|
1371
|
+
super();
|
|
1372
|
+
this._config = {
|
|
1373
|
+
currency: config.currency ?? 'USD',
|
|
1374
|
+
locale: config.locale ?? 'en-US',
|
|
1375
|
+
countupDuration: config.countupDuration ?? 1500,
|
|
1376
|
+
popScale: config.popScale ?? 1.2,
|
|
1377
|
+
};
|
|
1378
|
+
this._label = new Label({
|
|
1379
|
+
text: '',
|
|
1380
|
+
style: {
|
|
1381
|
+
fontSize: 48,
|
|
1382
|
+
fontWeight: 'bold',
|
|
1383
|
+
fill: 0xffd700,
|
|
1384
|
+
stroke: { color: 0x000000, width: 3 },
|
|
1385
|
+
...config.style,
|
|
1386
|
+
},
|
|
765
1387
|
});
|
|
1388
|
+
this.addChild(this._label);
|
|
1389
|
+
this.visible = false;
|
|
766
1390
|
}
|
|
767
1391
|
/**
|
|
768
|
-
*
|
|
1392
|
+
* Show a win with countup animation.
|
|
1393
|
+
*
|
|
1394
|
+
* @param amount - Win amount
|
|
1395
|
+
* @returns Promise that resolves when the animation completes
|
|
769
1396
|
*/
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1397
|
+
async showWin(amount) {
|
|
1398
|
+
this.visible = true;
|
|
1399
|
+
this.alpha = 1;
|
|
1400
|
+
// Cancel any running animation
|
|
1401
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1402
|
+
Tween.killTweensOf(this);
|
|
1403
|
+
// Setup countup
|
|
1404
|
+
this._tweenTarget.value = 0;
|
|
1405
|
+
this.scale.set(0.5);
|
|
1406
|
+
// Scale pop animation
|
|
1407
|
+
const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
|
|
1408
|
+
// Countup animation
|
|
1409
|
+
const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
|
|
1410
|
+
this.displayAmount(this._tweenTarget.value);
|
|
777
1411
|
});
|
|
1412
|
+
await Promise.all([scalePromise, countupPromise]);
|
|
1413
|
+
// Ensure final value is exact
|
|
1414
|
+
this.displayAmount(amount);
|
|
1415
|
+
this.scale.set(1);
|
|
778
1416
|
}
|
|
779
1417
|
/**
|
|
780
|
-
*
|
|
1418
|
+
* Skip the countup animation and show the final amount immediately.
|
|
781
1419
|
*/
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
/** Number of active tweens */
|
|
789
|
-
static get activeTweens() {
|
|
790
|
-
return Tween._tweens.length;
|
|
1420
|
+
skipCountup(amount) {
|
|
1421
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1422
|
+
Tween.killTweensOf(this);
|
|
1423
|
+
this.displayAmount(amount);
|
|
1424
|
+
this.scale.set(1);
|
|
791
1425
|
}
|
|
792
1426
|
/**
|
|
793
|
-
*
|
|
794
|
-
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
1427
|
+
* Hide the win display.
|
|
795
1428
|
*/
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (Tween._tickerAdded) {
|
|
802
|
-
pixi_js.Ticker.shared.remove(Tween.tick);
|
|
803
|
-
Tween._tickerAdded = false;
|
|
804
|
-
}
|
|
1429
|
+
hide() {
|
|
1430
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1431
|
+
Tween.killTweensOf(this);
|
|
1432
|
+
this.visible = false;
|
|
1433
|
+
this._label.text = '';
|
|
805
1434
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (Tween._tickerAdded)
|
|
809
|
-
return;
|
|
810
|
-
Tween._tickerAdded = true;
|
|
811
|
-
pixi_js.Ticker.shared.add(Tween.tick);
|
|
1435
|
+
displayAmount(amount) {
|
|
1436
|
+
this._label.setCurrency(amount, this._config.currency, this._config.locale);
|
|
812
1437
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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;
|
|
1438
|
+
/** React reconciler update hook */
|
|
1439
|
+
updateConfig(changed) {
|
|
1440
|
+
if ('currency' in changed)
|
|
1441
|
+
this._config.currency = changed.currency;
|
|
1442
|
+
if ('locale' in changed)
|
|
1443
|
+
this._config.locale = changed.locale;
|
|
857
1444
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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;
|
|
1445
|
+
destroy(options) {
|
|
1446
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1447
|
+
Tween.killTweensOf(this);
|
|
1448
|
+
super.destroy(options);
|
|
868
1449
|
}
|
|
869
1450
|
}
|
|
870
1451
|
|
|
@@ -872,7 +1453,7 @@ class Tween {
|
|
|
872
1453
|
* Modal overlay component.
|
|
873
1454
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
874
1455
|
*
|
|
875
|
-
*
|
|
1456
|
+
* Content is automatically centered via position calculations.
|
|
876
1457
|
*
|
|
877
1458
|
* @example
|
|
878
1459
|
* ```ts
|
|
@@ -883,6 +1464,7 @@ class Tween {
|
|
|
883
1464
|
* ```
|
|
884
1465
|
*/
|
|
885
1466
|
class Modal extends pixi_js.Container {
|
|
1467
|
+
__uiComponent = true;
|
|
886
1468
|
_overlay;
|
|
887
1469
|
_contentContainer;
|
|
888
1470
|
_config;
|
|
@@ -952,6 +1534,17 @@ class Modal extends pixi_js.Container {
|
|
|
952
1534
|
this._showing = false;
|
|
953
1535
|
this.onClose?.();
|
|
954
1536
|
}
|
|
1537
|
+
/** React reconciler update hook */
|
|
1538
|
+
updateConfig(changed) {
|
|
1539
|
+
if ('overlayAlpha' in changed)
|
|
1540
|
+
this._config.overlayAlpha = changed.overlayAlpha;
|
|
1541
|
+
if ('closeOnOverlay' in changed)
|
|
1542
|
+
this._config.closeOnOverlay = changed.closeOnOverlay;
|
|
1543
|
+
if ('animationDuration' in changed)
|
|
1544
|
+
this._config.animationDuration = changed.animationDuration;
|
|
1545
|
+
if ('onClose' in changed)
|
|
1546
|
+
this.onClose = changed.onClose;
|
|
1547
|
+
}
|
|
955
1548
|
}
|
|
956
1549
|
|
|
957
1550
|
const TOAST_COLORS = {
|
|
@@ -971,17 +1564,21 @@ const TOAST_COLORS = {
|
|
|
971
1564
|
* ```
|
|
972
1565
|
*/
|
|
973
1566
|
class Toast extends pixi_js.Container {
|
|
1567
|
+
__uiComponent = true;
|
|
974
1568
|
_bg;
|
|
1569
|
+
_customBg;
|
|
975
1570
|
_text;
|
|
976
1571
|
_config;
|
|
977
|
-
|
|
1572
|
+
_dismissPending = false;
|
|
978
1573
|
constructor(config = {}) {
|
|
979
1574
|
super();
|
|
980
1575
|
this._config = {
|
|
981
1576
|
duration: config.duration ?? 3000,
|
|
982
1577
|
bottomOffset: config.bottomOffset ?? 60,
|
|
983
1578
|
};
|
|
984
|
-
|
|
1579
|
+
const customBg = resolveView(config.backgroundView);
|
|
1580
|
+
this._customBg = !!customBg;
|
|
1581
|
+
this._bg = customBg ?? new pixi_js.Graphics();
|
|
985
1582
|
this.addChild(this._bg);
|
|
986
1583
|
this._text = new pixi_js.Text({
|
|
987
1584
|
text: '',
|
|
@@ -999,18 +1596,27 @@ class Toast extends pixi_js.Container {
|
|
|
999
1596
|
* Show a toast message.
|
|
1000
1597
|
*/
|
|
1001
1598
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1599
|
+
// Cancel any pending dismiss
|
|
1600
|
+
Tween.killTweensOf(this);
|
|
1601
|
+
this._dismissPending = false;
|
|
1005
1602
|
this._text.text = message;
|
|
1006
1603
|
const padding = 20;
|
|
1007
1604
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
1008
1605
|
const height = 44;
|
|
1009
1606
|
const radius = 8;
|
|
1010
1607
|
// Draw the background
|
|
1011
|
-
this.
|
|
1012
|
-
|
|
1013
|
-
|
|
1608
|
+
if (this._customBg) {
|
|
1609
|
+
this._bg.width = width;
|
|
1610
|
+
this._bg.height = height;
|
|
1611
|
+
this._bg.x = -width / 2;
|
|
1612
|
+
this._bg.y = -height / 2;
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
const g = this._bg;
|
|
1616
|
+
g.clear();
|
|
1617
|
+
g.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
1618
|
+
g.fill(TOAST_COLORS[type]);
|
|
1619
|
+
}
|
|
1014
1620
|
// Position
|
|
1015
1621
|
if (viewWidth && viewHeight) {
|
|
1016
1622
|
this.x = viewWidth / 2;
|
|
@@ -1021,9 +1627,12 @@ class Toast extends pixi_js.Container {
|
|
|
1021
1627
|
this.y += 20;
|
|
1022
1628
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
1023
1629
|
if (this._config.duration > 0) {
|
|
1024
|
-
this.
|
|
1025
|
-
|
|
1026
|
-
|
|
1630
|
+
this._dismissPending = true;
|
|
1631
|
+
await Tween.delay(this._config.duration);
|
|
1632
|
+
if (this._dismissPending) {
|
|
1633
|
+
this._dismissPending = false;
|
|
1634
|
+
await this.dismiss();
|
|
1635
|
+
}
|
|
1027
1636
|
}
|
|
1028
1637
|
}
|
|
1029
1638
|
/**
|
|
@@ -1032,57 +1641,36 @@ class Toast extends pixi_js.Container {
|
|
|
1032
1641
|
async dismiss() {
|
|
1033
1642
|
if (!this.visible)
|
|
1034
1643
|
return;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
this._dismissTimeout = null;
|
|
1038
|
-
}
|
|
1644
|
+
this._dismissPending = false;
|
|
1645
|
+
Tween.killTweensOf(this);
|
|
1039
1646
|
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
1040
1647
|
this.visible = false;
|
|
1041
1648
|
}
|
|
1649
|
+
/** React reconciler update hook */
|
|
1650
|
+
updateConfig(changed) {
|
|
1651
|
+
if ('duration' in changed)
|
|
1652
|
+
this._config.duration = changed.duration;
|
|
1653
|
+
if ('bottomOffset' in changed)
|
|
1654
|
+
this._config.bottomOffset = changed.bottomOffset;
|
|
1655
|
+
}
|
|
1656
|
+
destroy(options) {
|
|
1657
|
+
this._dismissPending = false;
|
|
1658
|
+
Tween.killTweensOf(this);
|
|
1659
|
+
super.destroy(options);
|
|
1660
|
+
}
|
|
1042
1661
|
}
|
|
1043
1662
|
|
|
1044
1663
|
// ─── Helpers ─────────────────────────────────────────────
|
|
1045
|
-
|
|
1046
|
-
start: 'flex-start',
|
|
1047
|
-
center: 'center',
|
|
1048
|
-
end: 'flex-end',
|
|
1049
|
-
stretch: 'stretch',
|
|
1050
|
-
};
|
|
1051
|
-
function normalizePadding(padding) {
|
|
1052
|
-
if (typeof padding === 'number')
|
|
1053
|
-
return [padding, padding, padding, padding];
|
|
1054
|
-
return padding;
|
|
1055
|
-
}
|
|
1056
|
-
function directionToFlexStyles(direction, maxWidth) {
|
|
1664
|
+
function directionToFlex(direction) {
|
|
1057
1665
|
switch (direction) {
|
|
1058
|
-
case 'horizontal':
|
|
1059
|
-
|
|
1060
|
-
case '
|
|
1061
|
-
|
|
1062
|
-
case 'grid':
|
|
1063
|
-
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
1064
|
-
case 'wrap':
|
|
1065
|
-
return {
|
|
1066
|
-
flexDirection: 'row',
|
|
1067
|
-
flexWrap: 'wrap',
|
|
1068
|
-
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
1069
|
-
};
|
|
1666
|
+
case 'horizontal': return { direction: 'row', wrap: false };
|
|
1667
|
+
case 'vertical': return { direction: 'column', wrap: false };
|
|
1668
|
+
case 'grid': return { direction: 'row', wrap: true };
|
|
1669
|
+
case 'wrap': return { direction: 'row', wrap: true };
|
|
1070
1670
|
}
|
|
1071
1671
|
}
|
|
1072
|
-
function buildLayoutStyles(config) {
|
|
1073
|
-
const [pt, pr, pb, pl] = config.padding;
|
|
1074
|
-
return {
|
|
1075
|
-
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
1076
|
-
gap: config.gap,
|
|
1077
|
-
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
1078
|
-
paddingTop: pt,
|
|
1079
|
-
paddingRight: pr,
|
|
1080
|
-
paddingBottom: pb,
|
|
1081
|
-
paddingLeft: pl,
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
1672
|
/**
|
|
1085
|
-
* Responsive layout container powered by
|
|
1673
|
+
* Responsive layout container powered by a lightweight built-in flex layout solver.
|
|
1086
1674
|
*
|
|
1087
1675
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
1088
1676
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -1109,6 +1697,7 @@ function buildLayoutStyles(config) {
|
|
|
1109
1697
|
* ```
|
|
1110
1698
|
*/
|
|
1111
1699
|
class Layout extends pixi_js.Container {
|
|
1700
|
+
__uiComponent = true;
|
|
1112
1701
|
_layoutConfig;
|
|
1113
1702
|
_padding;
|
|
1114
1703
|
_anchor;
|
|
@@ -1117,6 +1706,7 @@ class Layout extends pixi_js.Container {
|
|
|
1117
1706
|
_items = [];
|
|
1118
1707
|
_viewportWidth = 0;
|
|
1119
1708
|
_viewportHeight = 0;
|
|
1709
|
+
_flex;
|
|
1120
1710
|
constructor(config = {}) {
|
|
1121
1711
|
super();
|
|
1122
1712
|
this._layoutConfig = {
|
|
@@ -1126,7 +1716,7 @@ class Layout extends pixi_js.Container {
|
|
|
1126
1716
|
autoLayout: config.autoLayout ?? true,
|
|
1127
1717
|
columns: config.columns ?? 2,
|
|
1128
1718
|
};
|
|
1129
|
-
this._padding =
|
|
1719
|
+
this._padding = config.padding ?? 0;
|
|
1130
1720
|
this._anchor = config.anchor ?? 'top-left';
|
|
1131
1721
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
1132
1722
|
this._breakpoints = config.breakpoints
|
|
@@ -1134,14 +1724,18 @@ class Layout extends pixi_js.Container {
|
|
|
1134
1724
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
1135
1725
|
.sort((a, b) => a[0] - b[0])
|
|
1136
1726
|
: [];
|
|
1727
|
+
// Create internal FlexContainer
|
|
1728
|
+
this._flex = new FlexContainer();
|
|
1729
|
+
super.addChild(this._flex);
|
|
1137
1730
|
this.applyLayoutStyles();
|
|
1138
1731
|
}
|
|
1139
1732
|
/** Add an item to the layout */
|
|
1140
1733
|
addItem(child) {
|
|
1141
1734
|
this._items.push(child);
|
|
1142
|
-
this.
|
|
1143
|
-
|
|
1144
|
-
|
|
1735
|
+
const flexConfig = this.buildFlexItemConfig(child);
|
|
1736
|
+
this._flex.addFlexChild(child, flexConfig);
|
|
1737
|
+
if (this._layoutConfig.autoLayout) {
|
|
1738
|
+
this.applyLayoutStyles();
|
|
1145
1739
|
}
|
|
1146
1740
|
return this;
|
|
1147
1741
|
}
|
|
@@ -1150,15 +1744,13 @@ class Layout extends pixi_js.Container {
|
|
|
1150
1744
|
const idx = this._items.indexOf(child);
|
|
1151
1745
|
if (idx !== -1) {
|
|
1152
1746
|
this._items.splice(idx, 1);
|
|
1153
|
-
this.
|
|
1747
|
+
this._flex.removeFlexChild(child);
|
|
1154
1748
|
}
|
|
1155
1749
|
return this;
|
|
1156
1750
|
}
|
|
1157
1751
|
/** Remove all items */
|
|
1158
1752
|
clearItems() {
|
|
1159
|
-
|
|
1160
|
-
this.removeChild(item);
|
|
1161
|
-
}
|
|
1753
|
+
this._flex.clearFlexChildren();
|
|
1162
1754
|
this._items.length = 0;
|
|
1163
1755
|
return this;
|
|
1164
1756
|
}
|
|
@@ -1181,43 +1773,58 @@ class Layout extends pixi_js.Container {
|
|
|
1181
1773
|
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
1182
1774
|
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
1183
1775
|
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
1184
|
-
effective.
|
|
1185
|
-
const padding = effective.padding !== undefined
|
|
1186
|
-
? normalizePadding(effective.padding)
|
|
1187
|
-
: this._padding;
|
|
1776
|
+
const padding = effective.padding ?? this._padding;
|
|
1188
1777
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
1189
|
-
const
|
|
1190
|
-
this.
|
|
1778
|
+
const { direction: flexDir, wrap } = directionToFlex(direction);
|
|
1779
|
+
this._flex.setDirection(flexDir);
|
|
1780
|
+
this._flex.setJustifyContent('start');
|
|
1781
|
+
this._flex.setAlignItems(alignment);
|
|
1782
|
+
this._flex.setGap(gap);
|
|
1783
|
+
this._flex.setPadding(padding);
|
|
1784
|
+
// Wrap and maxWidth
|
|
1785
|
+
if (wrap) {
|
|
1786
|
+
this._flex._config.flexWrap = true;
|
|
1787
|
+
if (direction === 'grid' && maxWidth < Infinity) {
|
|
1788
|
+
this._flex._maxWidth = maxWidth;
|
|
1789
|
+
}
|
|
1790
|
+
if (maxWidth < Infinity) {
|
|
1791
|
+
this._flex._maxWidth = maxWidth;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
this._flex._config.flexWrap = false;
|
|
1796
|
+
}
|
|
1797
|
+
// Update grid child widths
|
|
1191
1798
|
if (direction === 'grid') {
|
|
1192
1799
|
for (const item of this._items) {
|
|
1193
|
-
this.
|
|
1800
|
+
const flexConfig = this.buildFlexItemConfig(item);
|
|
1801
|
+
item._flexConfig = flexConfig;
|
|
1194
1802
|
}
|
|
1195
1803
|
}
|
|
1804
|
+
// Set explicit size if we have viewport dimensions
|
|
1805
|
+
if (this._viewportWidth > 0 && this._viewportHeight > 0) {
|
|
1806
|
+
this._flex.resize(this._viewportWidth, this._viewportHeight);
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
this._flex.updateLayout();
|
|
1810
|
+
}
|
|
1196
1811
|
}
|
|
1197
|
-
|
|
1812
|
+
buildFlexItemConfig(_child) {
|
|
1198
1813
|
const effective = this.resolveConfig();
|
|
1814
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
1199
1815
|
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const styles = gap > 0
|
|
1205
|
-
? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
|
|
1206
|
-
: { width: `${(100 / columns).toFixed(2)}%` };
|
|
1207
|
-
if (child._layout) {
|
|
1208
|
-
child._layout.setStyle(styles);
|
|
1209
|
-
}
|
|
1210
|
-
else {
|
|
1211
|
-
child.layout = styles;
|
|
1816
|
+
if (direction === 'grid' && columns > 0) {
|
|
1817
|
+
// For grid, give each item a proportional width
|
|
1818
|
+
// The actual pixel width will be computed during layout
|
|
1819
|
+
return { flexGrow: 1 };
|
|
1212
1820
|
}
|
|
1821
|
+
return undefined;
|
|
1213
1822
|
}
|
|
1214
1823
|
applyAnchor() {
|
|
1215
1824
|
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
1216
1825
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
1217
1826
|
return;
|
|
1218
|
-
const
|
|
1219
|
-
const contentW = bounds.width * this.scale.x;
|
|
1220
|
-
const contentH = bounds.height * this.scale.y;
|
|
1827
|
+
const { width: contentW, height: contentH } = this._flex.getContentSize();
|
|
1221
1828
|
const vw = this._viewportWidth;
|
|
1222
1829
|
const vh = this._viewportHeight;
|
|
1223
1830
|
let anchorX = 0;
|
|
@@ -1240,8 +1847,8 @@ class Layout extends pixi_js.Container {
|
|
|
1240
1847
|
else {
|
|
1241
1848
|
anchorY = (vh - contentH) / 2;
|
|
1242
1849
|
}
|
|
1243
|
-
this.x = anchorX
|
|
1244
|
-
this.y = anchorY
|
|
1850
|
+
this.x = anchorX;
|
|
1851
|
+
this.y = anchorY;
|
|
1245
1852
|
}
|
|
1246
1853
|
resolveConfig() {
|
|
1247
1854
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
@@ -1254,18 +1861,34 @@ class Layout extends pixi_js.Container {
|
|
|
1254
1861
|
}
|
|
1255
1862
|
return {};
|
|
1256
1863
|
}
|
|
1864
|
+
/** React reconciler update hook */
|
|
1865
|
+
updateConfig(changed) {
|
|
1866
|
+
if ('direction' in changed)
|
|
1867
|
+
this._layoutConfig.direction = changed.direction;
|
|
1868
|
+
if ('gap' in changed)
|
|
1869
|
+
this._layoutConfig.gap = changed.gap;
|
|
1870
|
+
if ('alignment' in changed)
|
|
1871
|
+
this._layoutConfig.alignment = changed.alignment;
|
|
1872
|
+
if ('anchor' in changed)
|
|
1873
|
+
this._anchor = changed.anchor;
|
|
1874
|
+
if ('padding' in changed)
|
|
1875
|
+
this._padding = changed.padding;
|
|
1876
|
+
if ('columns' in changed)
|
|
1877
|
+
this._layoutConfig.columns = changed.columns;
|
|
1878
|
+
this.applyLayoutStyles();
|
|
1879
|
+
if (this._viewportWidth > 0)
|
|
1880
|
+
this.applyAnchor();
|
|
1881
|
+
}
|
|
1882
|
+
destroy(options) {
|
|
1883
|
+
this._items.length = 0;
|
|
1884
|
+
super.destroy(options);
|
|
1885
|
+
}
|
|
1257
1886
|
}
|
|
1258
1887
|
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
horizontal: 'horizontal',
|
|
1262
|
-
both: 'bidirectional',
|
|
1263
|
-
};
|
|
1888
|
+
const DECELERATION = 0.95;
|
|
1889
|
+
const MIN_VELOCITY = 0.5;
|
|
1264
1890
|
/**
|
|
1265
|
-
* Scrollable container
|
|
1266
|
-
*
|
|
1267
|
-
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
1268
|
-
* dynamic rendering optimization for off-screen items.
|
|
1891
|
+
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
1269
1892
|
*
|
|
1270
1893
|
* @example
|
|
1271
1894
|
* ```ts
|
|
@@ -1283,58 +1906,355 @@ const DIRECTION_MAP = {
|
|
|
1283
1906
|
* scene.container.addChild(scroll);
|
|
1284
1907
|
* ```
|
|
1285
1908
|
*/
|
|
1286
|
-
class ScrollContainer extends
|
|
1909
|
+
class ScrollContainer extends pixi_js.Container {
|
|
1910
|
+
__uiComponent = true;
|
|
1911
|
+
_viewport;
|
|
1912
|
+
_internalSetup = true;
|
|
1913
|
+
_content;
|
|
1914
|
+
_maskGfx;
|
|
1915
|
+
_bg = null;
|
|
1287
1916
|
_scrollConfig;
|
|
1917
|
+
_items = [];
|
|
1918
|
+
// Scrollbar
|
|
1919
|
+
_scrollbar = null;
|
|
1920
|
+
_scrollbarConfig;
|
|
1921
|
+
// Drag state
|
|
1922
|
+
_dragging = false;
|
|
1923
|
+
_dragStart = { x: 0, y: 0 };
|
|
1924
|
+
_contentStart = { x: 0, y: 0 };
|
|
1925
|
+
_velocity = { x: 0, y: 0 };
|
|
1926
|
+
_lastDragPos = { x: 0, y: 0 };
|
|
1927
|
+
_lastDragTime = 0;
|
|
1928
|
+
_inertiaActive = false;
|
|
1929
|
+
// Bound handlers for cleanup
|
|
1930
|
+
_onTickBound = null;
|
|
1931
|
+
_onWheelBound = null;
|
|
1288
1932
|
constructor(config) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
radius: config.borderRadius ?? 0,
|
|
1933
|
+
super();
|
|
1934
|
+
this._viewport = { width: config.width, height: config.height };
|
|
1935
|
+
this._scrollConfig = {
|
|
1936
|
+
direction: config.direction ?? 'vertical',
|
|
1294
1937
|
elementsMargin: config.elementsMargin ?? 0,
|
|
1295
1938
|
padding: config.padding ?? 0,
|
|
1296
|
-
|
|
1939
|
+
borderRadius: config.borderRadius ?? 0,
|
|
1297
1940
|
disableEasing: config.disableEasing ?? false,
|
|
1298
|
-
globalScroll: config.globalScroll ?? true,
|
|
1299
1941
|
};
|
|
1942
|
+
// Background
|
|
1300
1943
|
if (config.backgroundColor !== undefined) {
|
|
1301
|
-
|
|
1944
|
+
this._bg = new pixi_js.Graphics();
|
|
1945
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
1946
|
+
.fill(config.backgroundColor);
|
|
1947
|
+
this.addChild(this._bg);
|
|
1948
|
+
}
|
|
1949
|
+
// Mask
|
|
1950
|
+
this._maskGfx = new pixi_js.Graphics();
|
|
1951
|
+
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
1952
|
+
.fill(0xffffff);
|
|
1953
|
+
this.addChild(this._maskGfx);
|
|
1954
|
+
// Content container
|
|
1955
|
+
this._content = new pixi_js.Container();
|
|
1956
|
+
this._content.mask = this._maskGfx;
|
|
1957
|
+
this.addChild(this._content);
|
|
1958
|
+
// Interaction
|
|
1959
|
+
this.eventMode = 'static';
|
|
1960
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
1961
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
1962
|
+
this.on('pointermove', this._onPointerMove, this);
|
|
1963
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
1964
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
1965
|
+
// Mouse wheel
|
|
1966
|
+
this._onWheelBound = this._onWheel.bind(this);
|
|
1967
|
+
// Scrollbar
|
|
1968
|
+
const sbWidth = config.scrollbarWidth ?? 6;
|
|
1969
|
+
const sbPadding = config.scrollbarPadding ?? 4;
|
|
1970
|
+
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
1971
|
+
if (config.scrollbar) {
|
|
1972
|
+
const customThumb = resolveView(config.thumbView);
|
|
1973
|
+
if (customThumb) {
|
|
1974
|
+
this._scrollbar = customThumb;
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
const g = new pixi_js.Graphics();
|
|
1978
|
+
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
1979
|
+
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
1980
|
+
this._scrollbar = g;
|
|
1981
|
+
}
|
|
1982
|
+
this._scrollbar.visible = false;
|
|
1983
|
+
super.addChild(this._scrollbar);
|
|
1302
1984
|
}
|
|
1303
|
-
|
|
1304
|
-
this._scrollConfig = config;
|
|
1985
|
+
this._internalSetup = false;
|
|
1305
1986
|
}
|
|
1306
|
-
/**
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1987
|
+
/**
|
|
1988
|
+
* Override addChild so external children are routed to scroll content.
|
|
1989
|
+
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
1990
|
+
*/
|
|
1991
|
+
addChild(...children) {
|
|
1992
|
+
if (this._internalSetup) {
|
|
1993
|
+
return super.addChild(...children);
|
|
1994
|
+
}
|
|
1995
|
+
for (const child of children) {
|
|
1996
|
+
this.addItem(child);
|
|
1997
|
+
}
|
|
1998
|
+
return children[0];
|
|
1999
|
+
}
|
|
2000
|
+
removeChild(...children) {
|
|
2001
|
+
if (this._internalSetup) {
|
|
2002
|
+
return super.removeChild(...children);
|
|
2003
|
+
}
|
|
2004
|
+
for (const child of children) {
|
|
2005
|
+
const idx = this._items.indexOf(child);
|
|
2006
|
+
if (idx !== -1) {
|
|
2007
|
+
this._items.splice(idx, 1);
|
|
2008
|
+
this._content.removeChild(child);
|
|
1313
2009
|
}
|
|
1314
2010
|
}
|
|
1315
|
-
|
|
2011
|
+
this.layoutItems();
|
|
2012
|
+
return children[0];
|
|
2013
|
+
}
|
|
2014
|
+
/** React reconciler update hook */
|
|
2015
|
+
updateConfig(changed) {
|
|
2016
|
+
if ('width' in changed || 'height' in changed) {
|
|
2017
|
+
this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
2021
|
+
enableWheel(canvas) {
|
|
2022
|
+
if (this._onWheelBound) {
|
|
2023
|
+
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/** Set scrollable content. Replaces any existing items. */
|
|
2027
|
+
setContent(content) {
|
|
2028
|
+
this.clearItems();
|
|
1316
2029
|
const children = [...content.children];
|
|
1317
|
-
|
|
1318
|
-
this.
|
|
2030
|
+
for (const child of children) {
|
|
2031
|
+
this.addItem(child);
|
|
1319
2032
|
}
|
|
1320
2033
|
}
|
|
1321
2034
|
/** Add a single item */
|
|
1322
|
-
addItem(
|
|
1323
|
-
this.
|
|
1324
|
-
|
|
2035
|
+
addItem(child) {
|
|
2036
|
+
this._items.push(child);
|
|
2037
|
+
this._content.addChild(child);
|
|
2038
|
+
this.layoutItems();
|
|
2039
|
+
return this;
|
|
1325
2040
|
}
|
|
1326
|
-
/**
|
|
2041
|
+
/** Remove all items */
|
|
2042
|
+
clearItems() {
|
|
2043
|
+
for (const item of this._items) {
|
|
2044
|
+
this._content.removeChild(item);
|
|
2045
|
+
}
|
|
2046
|
+
this._items.length = 0;
|
|
2047
|
+
}
|
|
2048
|
+
/** Get items */
|
|
2049
|
+
get items() {
|
|
2050
|
+
return this._items;
|
|
2051
|
+
}
|
|
2052
|
+
/** Scroll to make a specific item index visible */
|
|
1327
2053
|
scrollToItem(index) {
|
|
1328
|
-
this.
|
|
2054
|
+
if (index < 0 || index >= this._items.length)
|
|
2055
|
+
return;
|
|
2056
|
+
const item = this._items[index];
|
|
2057
|
+
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
2058
|
+
if (isVert) {
|
|
2059
|
+
this._content.y = -item.y + this._scrollConfig.padding;
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
this._content.x = -item.x + this._scrollConfig.padding;
|
|
2063
|
+
}
|
|
2064
|
+
this.clampScroll();
|
|
1329
2065
|
}
|
|
1330
2066
|
/** Current scroll position */
|
|
1331
2067
|
get scrollPosition() {
|
|
1332
|
-
return { x: this.
|
|
2068
|
+
return { x: this._content.x, y: this._content.y };
|
|
2069
|
+
}
|
|
2070
|
+
/** Resize the scroll viewport */
|
|
2071
|
+
setViewportSize(width, height) {
|
|
2072
|
+
this._viewport.width = width;
|
|
2073
|
+
this._viewport.height = height;
|
|
2074
|
+
this._maskGfx.clear();
|
|
2075
|
+
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
2076
|
+
if (this._bg) {
|
|
2077
|
+
this._bg.clear();
|
|
2078
|
+
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
2079
|
+
.fill(0xffffff); // color will be overridden if needed
|
|
2080
|
+
}
|
|
2081
|
+
this.clampScroll();
|
|
2082
|
+
}
|
|
2083
|
+
// ─── Layout ──────────────────────────────────────────
|
|
2084
|
+
layoutItems() {
|
|
2085
|
+
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
2086
|
+
const isVert = direction !== 'horizontal';
|
|
2087
|
+
let pos = padding;
|
|
2088
|
+
for (const item of this._items) {
|
|
2089
|
+
if (isVert) {
|
|
2090
|
+
item.x = padding;
|
|
2091
|
+
item.y = pos;
|
|
2092
|
+
pos += item.height + elementsMargin;
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
item.x = pos;
|
|
2096
|
+
item.y = padding;
|
|
2097
|
+
pos += item.width + elementsMargin;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// ─── Drag handling ───────────────────────────────────
|
|
2102
|
+
_onPointerDown(e) {
|
|
2103
|
+
this._dragging = true;
|
|
2104
|
+
this._inertiaActive = false;
|
|
2105
|
+
this._dragStart.x = e.globalX;
|
|
2106
|
+
this._dragStart.y = e.globalY;
|
|
2107
|
+
this._contentStart.x = this._content.x;
|
|
2108
|
+
this._contentStart.y = this._content.y;
|
|
2109
|
+
this._lastDragPos.x = e.globalX;
|
|
2110
|
+
this._lastDragPos.y = e.globalY;
|
|
2111
|
+
this._lastDragTime = Date.now();
|
|
2112
|
+
this._velocity.x = 0;
|
|
2113
|
+
this._velocity.y = 0;
|
|
2114
|
+
this.stopInertia();
|
|
2115
|
+
}
|
|
2116
|
+
_onPointerMove(e) {
|
|
2117
|
+
if (!this._dragging)
|
|
2118
|
+
return;
|
|
2119
|
+
const dx = e.globalX - this._dragStart.x;
|
|
2120
|
+
const dy = e.globalY - this._dragStart.y;
|
|
2121
|
+
const { direction } = this._scrollConfig;
|
|
2122
|
+
if (direction !== 'horizontal') {
|
|
2123
|
+
this._content.y = this._contentStart.y + dy;
|
|
2124
|
+
}
|
|
2125
|
+
if (direction !== 'vertical') {
|
|
2126
|
+
this._content.x = this._contentStart.x + dx;
|
|
2127
|
+
}
|
|
2128
|
+
// Track velocity
|
|
2129
|
+
const now = Date.now();
|
|
2130
|
+
const dt = now - this._lastDragTime;
|
|
2131
|
+
if (dt > 0) {
|
|
2132
|
+
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
2133
|
+
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
2134
|
+
}
|
|
2135
|
+
this._lastDragPos.x = e.globalX;
|
|
2136
|
+
this._lastDragPos.y = e.globalY;
|
|
2137
|
+
this._lastDragTime = now;
|
|
2138
|
+
this.clampScroll();
|
|
2139
|
+
}
|
|
2140
|
+
_onPointerUp() {
|
|
2141
|
+
if (!this._dragging)
|
|
2142
|
+
return;
|
|
2143
|
+
this._dragging = false;
|
|
2144
|
+
if (!this._scrollConfig.disableEasing &&
|
|
2145
|
+
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
2146
|
+
this.startInertia();
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
// ─── Inertia ─────────────────────────────────────────
|
|
2150
|
+
startInertia() {
|
|
2151
|
+
this._inertiaActive = true;
|
|
2152
|
+
this._onTickBound = this._inertiaTick.bind(this);
|
|
2153
|
+
pixi_js.Ticker.shared.add(this._onTickBound);
|
|
2154
|
+
}
|
|
2155
|
+
stopInertia() {
|
|
2156
|
+
if (this._onTickBound && this._inertiaActive) {
|
|
2157
|
+
pixi_js.Ticker.shared.remove(this._onTickBound);
|
|
2158
|
+
this._inertiaActive = false;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
_inertiaTick() {
|
|
2162
|
+
const { direction } = this._scrollConfig;
|
|
2163
|
+
if (direction !== 'horizontal') {
|
|
2164
|
+
this._content.y += this._velocity.y;
|
|
2165
|
+
this._velocity.y *= DECELERATION;
|
|
2166
|
+
}
|
|
2167
|
+
if (direction !== 'vertical') {
|
|
2168
|
+
this._content.x += this._velocity.x;
|
|
2169
|
+
this._velocity.x *= DECELERATION;
|
|
2170
|
+
}
|
|
2171
|
+
this.clampScroll();
|
|
2172
|
+
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
2173
|
+
this.stopInertia();
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
// ─── Mouse wheel ─────────────────────────────────────
|
|
2177
|
+
_onWheel(e) {
|
|
2178
|
+
const { direction } = this._scrollConfig;
|
|
2179
|
+
e.preventDefault();
|
|
2180
|
+
if (direction !== 'horizontal') {
|
|
2181
|
+
this._content.y -= e.deltaY;
|
|
2182
|
+
}
|
|
2183
|
+
if (direction !== 'vertical') {
|
|
2184
|
+
this._content.x -= e.deltaX;
|
|
2185
|
+
}
|
|
2186
|
+
this.clampScroll();
|
|
2187
|
+
}
|
|
2188
|
+
// ─── Scroll bounds ───────────────────────────────────
|
|
2189
|
+
clampScroll() {
|
|
2190
|
+
const { direction } = this._scrollConfig;
|
|
2191
|
+
const bounds = this._content.getLocalBounds();
|
|
2192
|
+
if (direction !== 'horizontal') {
|
|
2193
|
+
const contentHeight = bounds.height + bounds.y;
|
|
2194
|
+
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
2195
|
+
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
2196
|
+
}
|
|
2197
|
+
if (direction !== 'vertical') {
|
|
2198
|
+
const contentWidth = bounds.width + bounds.x;
|
|
2199
|
+
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
2200
|
+
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
2201
|
+
}
|
|
2202
|
+
this.updateScrollbar();
|
|
2203
|
+
}
|
|
2204
|
+
updateScrollbar() {
|
|
2205
|
+
if (!this._scrollbar)
|
|
2206
|
+
return;
|
|
2207
|
+
const { direction } = this._scrollConfig;
|
|
2208
|
+
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
2209
|
+
const bounds = this._content.getLocalBounds();
|
|
2210
|
+
const isVert = direction !== 'horizontal';
|
|
2211
|
+
if (isVert) {
|
|
2212
|
+
const contentH = bounds.height + bounds.y;
|
|
2213
|
+
if (contentH <= this._viewport.height) {
|
|
2214
|
+
this._scrollbar.visible = false;
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
this._scrollbar.visible = true;
|
|
2218
|
+
const ratio = this._viewport.height / contentH;
|
|
2219
|
+
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
2220
|
+
const scrollRange = this._viewport.height - thumbH;
|
|
2221
|
+
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
2222
|
+
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
2223
|
+
this._scrollbar.y = scrollProgress * scrollRange;
|
|
2224
|
+
this._scrollbar.height = thumbH;
|
|
2225
|
+
this._scrollbar.width = sbW;
|
|
2226
|
+
}
|
|
2227
|
+
else {
|
|
2228
|
+
const contentW = bounds.width + bounds.x;
|
|
2229
|
+
if (contentW <= this._viewport.width) {
|
|
2230
|
+
this._scrollbar.visible = false;
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
this._scrollbar.visible = true;
|
|
2234
|
+
const ratio = this._viewport.width / contentW;
|
|
2235
|
+
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
2236
|
+
const scrollRange = this._viewport.width - thumbW;
|
|
2237
|
+
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
2238
|
+
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
2239
|
+
this._scrollbar.x = scrollProgress * scrollRange;
|
|
2240
|
+
this._scrollbar.width = thumbW;
|
|
2241
|
+
this._scrollbar.height = sbW;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
destroy(options) {
|
|
2245
|
+
this.stopInertia();
|
|
2246
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
2247
|
+
this.off('pointermove', this._onPointerMove, this);
|
|
2248
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
2249
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
2250
|
+
this._items.length = 0;
|
|
2251
|
+
super.destroy(options);
|
|
1333
2252
|
}
|
|
1334
2253
|
}
|
|
1335
2254
|
|
|
1336
2255
|
exports.BalanceDisplay = BalanceDisplay;
|
|
1337
2256
|
exports.Button = Button;
|
|
2257
|
+
exports.FlexContainer = FlexContainer;
|
|
1338
2258
|
exports.Label = Label;
|
|
1339
2259
|
exports.Layout = Layout;
|
|
1340
2260
|
exports.Modal = Modal;
|
|
@@ -1343,4 +2263,5 @@ exports.ProgressBar = ProgressBar;
|
|
|
1343
2263
|
exports.ScrollContainer = ScrollContainer;
|
|
1344
2264
|
exports.Toast = Toast;
|
|
1345
2265
|
exports.WinDisplay = WinDisplay;
|
|
2266
|
+
exports.resolveView = resolveView;
|
|
1346
2267
|
//# sourceMappingURL=ui.cjs.js.map
|