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