@energy8platform/game-engine 0.10.11 → 0.12.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 +272 -74
- package/dist/index.cjs.js +1322 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +369 -46
- package/dist/index.esm.js +1323 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +8 -18
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +8 -18
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2848 -35
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2848 -36
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1913 -592
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +528 -46
- package/dist/ui.esm.js +1911 -594
- 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/react/applyProps.ts +90 -2
- package/src/react/extendAll.ts +29 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +249 -0
- package/src/react/reconciler.ts +80 -7
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +529 -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/Slider.ts +241 -0
- package/src/ui/Toast.ts +47 -17
- package/src/ui/Toggle.ts +201 -0
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +9 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
package/dist/ui.esm.js
CHANGED
|
@@ -1,7 +1,657 @@
|
|
|
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
|
+
// Shrink: if content overflows and mainSize is finite, shrink eligible items
|
|
83
|
+
if (totalGrow === 0 && mainSize > 0) {
|
|
84
|
+
const overflow = totalFixed + totalGap - mainSize;
|
|
85
|
+
if (overflow > 0) {
|
|
86
|
+
let totalShrinkable = 0;
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
89
|
+
if (shrink > 0) {
|
|
90
|
+
totalShrinkable += isRow ? item.w : item.h;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (totalShrinkable > 0) {
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
96
|
+
if (shrink > 0) {
|
|
97
|
+
const itemMain = isRow ? item.w : item.h;
|
|
98
|
+
const reduction = overflow * (itemMain / totalShrinkable);
|
|
99
|
+
const newSize = Math.max(0, itemMain - reduction);
|
|
100
|
+
if (isRow) {
|
|
101
|
+
item.w = newSize;
|
|
102
|
+
item.child.width = newSize;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
item.h = newSize;
|
|
106
|
+
item.child.height = newSize;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Calculate total main size after flex
|
|
114
|
+
let totalMain = totalGap;
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
totalMain += isRow ? item.w : item.h;
|
|
117
|
+
}
|
|
118
|
+
// Justify: compute starting offset and extra spacing
|
|
119
|
+
let mainOffset = 0;
|
|
120
|
+
let extraGap = 0;
|
|
121
|
+
switch (justify) {
|
|
122
|
+
case 'start':
|
|
123
|
+
break;
|
|
124
|
+
case 'center':
|
|
125
|
+
mainOffset = Math.max(0, (mainSize - totalMain) / 2);
|
|
126
|
+
break;
|
|
127
|
+
case 'end':
|
|
128
|
+
mainOffset = Math.max(0, mainSize - totalMain);
|
|
129
|
+
break;
|
|
130
|
+
case 'space-between':
|
|
131
|
+
if (items.length > 1) {
|
|
132
|
+
extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'space-around':
|
|
136
|
+
if (items.length > 0) {
|
|
137
|
+
const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
|
|
138
|
+
const segment = totalSpace / items.length;
|
|
139
|
+
mainOffset = segment / 2;
|
|
140
|
+
extraGap = segment - gap;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
// Position each item
|
|
145
|
+
let pos = mainOffset;
|
|
146
|
+
for (const item of items) {
|
|
147
|
+
const mainDim = isRow ? item.w : item.h;
|
|
148
|
+
const crossDim = isRow ? item.h : item.w;
|
|
149
|
+
// Cross-axis alignment (alignSelf overrides align)
|
|
150
|
+
const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
|
|
151
|
+
? item.child._flexConfig.alignSelf
|
|
152
|
+
: align;
|
|
153
|
+
let crossPos = crossOffset;
|
|
154
|
+
switch (effectiveAlign) {
|
|
155
|
+
case 'start':
|
|
156
|
+
break;
|
|
157
|
+
case 'center':
|
|
158
|
+
crossPos += (crossSize - crossDim) / 2;
|
|
159
|
+
break;
|
|
160
|
+
case 'end':
|
|
161
|
+
crossPos += crossSize - crossDim;
|
|
162
|
+
break;
|
|
163
|
+
case 'stretch':
|
|
164
|
+
if (isRow) {
|
|
165
|
+
item.child.height = crossSize;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
item.child.width = crossSize;
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
// Compensate for local bounds offset (e.g. centered anchors)
|
|
173
|
+
if (isRow) {
|
|
174
|
+
item.child.x = pos - item.ox;
|
|
175
|
+
item.child.y = crossPos - item.oy;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
item.child.x = crossPos - item.ox;
|
|
179
|
+
item.child.y = pos - item.oy;
|
|
180
|
+
}
|
|
181
|
+
pos += mainDim + gap + extraGap;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ─── FlexContainer ───────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Lightweight flexbox-like layout container for PixiJS.
|
|
187
|
+
*
|
|
188
|
+
* Supports row/column direction, justify/align, gap, padding, wrapping,
|
|
189
|
+
* and flex-grow distribution. Zero external dependencies.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* const toolbar = new FlexContainer({
|
|
194
|
+
* direction: 'row',
|
|
195
|
+
* justifyContent: 'space-between',
|
|
196
|
+
* alignItems: 'center',
|
|
197
|
+
* gap: 16,
|
|
198
|
+
* padding: 12,
|
|
199
|
+
* });
|
|
200
|
+
*
|
|
201
|
+
* toolbar.addFlexChild(button1);
|
|
202
|
+
* toolbar.addFlexChild(button2);
|
|
203
|
+
* toolbar.resize(800, 60);
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
class FlexContainer extends Container {
|
|
207
|
+
__uiComponent = true;
|
|
208
|
+
_config;
|
|
209
|
+
_padding;
|
|
210
|
+
_maxWidth;
|
|
211
|
+
_maxHeight;
|
|
212
|
+
/** @internal */ _explicitWidth;
|
|
213
|
+
/** @internal */ _explicitHeight;
|
|
214
|
+
_layoutChildren = [];
|
|
215
|
+
_layoutDirty = true;
|
|
216
|
+
constructor(config = {}) {
|
|
217
|
+
super();
|
|
218
|
+
this._config = {
|
|
219
|
+
direction: config.direction ?? 'row',
|
|
220
|
+
justifyContent: config.justifyContent ?? 'start',
|
|
221
|
+
alignItems: config.alignItems ?? 'start',
|
|
222
|
+
gap: config.gap ?? 0,
|
|
223
|
+
flexWrap: config.flexWrap ?? false,
|
|
224
|
+
};
|
|
225
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
226
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
227
|
+
this._maxHeight = config.maxHeight ?? Infinity;
|
|
228
|
+
this._explicitWidth = config.width ?? 0;
|
|
229
|
+
this._explicitHeight = config.height ?? 0;
|
|
230
|
+
}
|
|
231
|
+
// ─── Public API ──────────────────────────────────────
|
|
232
|
+
/** Add a child with optional flex config. Also registers in flex layout. */
|
|
233
|
+
addFlexChild(child, flexConfig) {
|
|
234
|
+
if (flexConfig)
|
|
235
|
+
child._flexConfig = flexConfig;
|
|
236
|
+
if (!this._layoutChildren.includes(child)) {
|
|
237
|
+
this._layoutChildren.push(child);
|
|
238
|
+
this._layoutDirty = true;
|
|
239
|
+
}
|
|
240
|
+
super.addChild(child);
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
/** Remove a child from flex layout and display list */
|
|
244
|
+
removeFlexChild(child) {
|
|
245
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
246
|
+
if (idx !== -1) {
|
|
247
|
+
this._layoutChildren.splice(idx, 1);
|
|
248
|
+
this._layoutDirty = true;
|
|
249
|
+
}
|
|
250
|
+
super.removeChild(child);
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
/** Remove all flex children */
|
|
254
|
+
clearFlexChildren() {
|
|
255
|
+
for (const child of this._layoutChildren) {
|
|
256
|
+
super.removeChild(child);
|
|
257
|
+
}
|
|
258
|
+
this._layoutChildren.length = 0;
|
|
259
|
+
this._layoutDirty = true;
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Override addChild so children automatically participate in flex layout.
|
|
264
|
+
* This enables declarative usage from React JSX.
|
|
265
|
+
*/
|
|
266
|
+
addChild(...children) {
|
|
267
|
+
for (const child of children) {
|
|
268
|
+
if (!this._layoutChildren.includes(child)) {
|
|
269
|
+
this._layoutChildren.push(child);
|
|
270
|
+
this._layoutDirty = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const result = super.addChild(...children);
|
|
274
|
+
if (this._layoutDirty)
|
|
275
|
+
this.updateLayout();
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
removeChild(...children) {
|
|
279
|
+
for (const child of children) {
|
|
280
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
281
|
+
if (idx !== -1) {
|
|
282
|
+
this._layoutChildren.splice(idx, 1);
|
|
283
|
+
this._layoutDirty = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return super.removeChild(...children);
|
|
287
|
+
}
|
|
288
|
+
/** Get all flex layout children (read-only) */
|
|
289
|
+
get flexChildren() {
|
|
290
|
+
return this._layoutChildren;
|
|
291
|
+
}
|
|
292
|
+
/** Update the container size and recalculate layout */
|
|
293
|
+
resize(width, height) {
|
|
294
|
+
this._explicitWidth = width;
|
|
295
|
+
this._explicitHeight = height;
|
|
296
|
+
this._layoutDirty = true;
|
|
297
|
+
this.updateLayout();
|
|
298
|
+
}
|
|
299
|
+
/** Update layout direction */
|
|
300
|
+
setDirection(direction) {
|
|
301
|
+
this._config.direction = direction;
|
|
302
|
+
this._layoutDirty = true;
|
|
303
|
+
}
|
|
304
|
+
/** Update justifyContent */
|
|
305
|
+
setJustifyContent(justify) {
|
|
306
|
+
this._config.justifyContent = justify;
|
|
307
|
+
this._layoutDirty = true;
|
|
308
|
+
}
|
|
309
|
+
/** Update alignItems */
|
|
310
|
+
setAlignItems(align) {
|
|
311
|
+
this._config.alignItems = align;
|
|
312
|
+
this._layoutDirty = true;
|
|
313
|
+
}
|
|
314
|
+
/** Update gap */
|
|
315
|
+
setGap(gap) {
|
|
316
|
+
this._config.gap = gap;
|
|
317
|
+
this._layoutDirty = true;
|
|
318
|
+
}
|
|
319
|
+
/** Update padding */
|
|
320
|
+
setPadding(padding) {
|
|
321
|
+
this._padding = normalizePadding(padding);
|
|
322
|
+
this._layoutDirty = true;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Recalculate and apply layout positions for all children.
|
|
326
|
+
* Called automatically by `resize()`. Call manually after
|
|
327
|
+
* adding/removing children without resize.
|
|
328
|
+
*/
|
|
329
|
+
updateLayout() {
|
|
330
|
+
this._layoutDirty = false;
|
|
331
|
+
const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
|
|
332
|
+
const [pt, pr, pb, pl] = this._padding;
|
|
333
|
+
const isRow = direction === 'row';
|
|
334
|
+
const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
|
|
335
|
+
const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
|
|
336
|
+
const mainLimit = isRow ? contentW : contentH;
|
|
337
|
+
const crossLimit = isRow ? contentH : contentW;
|
|
338
|
+
// Measure children (skip flexExclude — they position themselves)
|
|
339
|
+
const measured = [];
|
|
340
|
+
for (const child of this._layoutChildren) {
|
|
341
|
+
if (child._flexConfig?.flexExclude)
|
|
342
|
+
continue;
|
|
343
|
+
const { w, h, ox, oy } = measureChild(child);
|
|
344
|
+
measured.push({ child, w, h, ox, oy });
|
|
345
|
+
}
|
|
346
|
+
// Split into lines (if wrapping)
|
|
347
|
+
const lines = [];
|
|
348
|
+
if (flexWrap && mainLimit < Infinity) {
|
|
349
|
+
let currentLine = [];
|
|
350
|
+
let lineMain = 0;
|
|
351
|
+
for (const item of measured) {
|
|
352
|
+
const itemMain = isRow ? item.w : item.h;
|
|
353
|
+
const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
|
|
354
|
+
if (currentLine.length > 0 && wouldBe > mainLimit) {
|
|
355
|
+
lines.push(currentLine);
|
|
356
|
+
currentLine = [item];
|
|
357
|
+
lineMain = itemMain;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
currentLine.push(item);
|
|
361
|
+
lineMain = wouldBe;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (currentLine.length > 0)
|
|
365
|
+
lines.push(currentLine);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
lines.push(measured);
|
|
369
|
+
}
|
|
370
|
+
// Compute cross size per line
|
|
371
|
+
const lineCrossSizes = lines.map((line) => {
|
|
372
|
+
let maxCross = 0;
|
|
373
|
+
for (const item of line) {
|
|
374
|
+
const cross = isRow ? item.h : item.w;
|
|
375
|
+
if (cross > maxCross)
|
|
376
|
+
maxCross = cross;
|
|
377
|
+
}
|
|
378
|
+
return maxCross;
|
|
379
|
+
});
|
|
380
|
+
// Layout each line
|
|
381
|
+
let crossOffset = isRow ? pt : pl;
|
|
382
|
+
for (let i = 0; i < lines.length; i++) {
|
|
383
|
+
const line = lines[i];
|
|
384
|
+
const lineCross = lineCrossSizes[i];
|
|
385
|
+
const mainStart = isRow ? pl : pt;
|
|
386
|
+
// Offset items by padding
|
|
387
|
+
const tempItems = line.map((item) => ({ ...item }));
|
|
388
|
+
// For single-line layouts, use the full available cross space for alignment;
|
|
389
|
+
// for multi-line (wrapping), each line gets its own measured cross size.
|
|
390
|
+
const effectiveCross = lines.length === 1 && crossLimit < Infinity
|
|
391
|
+
? crossLimit
|
|
392
|
+
: (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
393
|
+
layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
|
|
394
|
+
// Apply main-axis padding offset
|
|
395
|
+
for (const item of tempItems) {
|
|
396
|
+
const origChild = line.find((l) => l.child === item.child);
|
|
397
|
+
origChild.child.x = item.child.x + (isRow ? mainStart : 0);
|
|
398
|
+
origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
|
|
399
|
+
}
|
|
400
|
+
crossOffset += lineCross + gap;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** Computed content size (after layout) */
|
|
404
|
+
getContentSize() {
|
|
405
|
+
if (this._layoutDirty)
|
|
406
|
+
this.updateLayout();
|
|
407
|
+
let maxX = 0;
|
|
408
|
+
let maxY = 0;
|
|
409
|
+
for (const child of this._layoutChildren) {
|
|
410
|
+
const { w, h } = measureChild(child);
|
|
411
|
+
maxX = Math.max(maxX, child.x + w);
|
|
412
|
+
maxY = Math.max(maxY, child.y + h);
|
|
413
|
+
}
|
|
414
|
+
const [, pr, pb] = this._padding;
|
|
415
|
+
return { width: maxX + pr, height: maxY + pb };
|
|
416
|
+
}
|
|
417
|
+
/** React reconciler update hook — applies changed config props */
|
|
418
|
+
updateConfig(changed) {
|
|
419
|
+
if ('direction' in changed)
|
|
420
|
+
this.setDirection(changed.direction);
|
|
421
|
+
if ('justifyContent' in changed)
|
|
422
|
+
this.setJustifyContent(changed.justifyContent);
|
|
423
|
+
if ('alignItems' in changed)
|
|
424
|
+
this.setAlignItems(changed.alignItems);
|
|
425
|
+
if ('gap' in changed)
|
|
426
|
+
this.setGap(changed.gap);
|
|
427
|
+
if ('padding' in changed)
|
|
428
|
+
this.setPadding(changed.padding);
|
|
429
|
+
if ('flexWrap' in changed) {
|
|
430
|
+
this._config.flexWrap = changed.flexWrap;
|
|
431
|
+
this._layoutDirty = true;
|
|
432
|
+
}
|
|
433
|
+
if ('width' in changed || 'height' in changed) {
|
|
434
|
+
this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
|
|
435
|
+
return; // resize calls updateLayout
|
|
436
|
+
}
|
|
437
|
+
if (this._layoutDirty)
|
|
438
|
+
this.updateLayout();
|
|
439
|
+
}
|
|
440
|
+
destroy(options) {
|
|
441
|
+
this._layoutChildren.length = 0;
|
|
442
|
+
super.destroy(options);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Collection of easing functions for use with Tween and Timeline.
|
|
448
|
+
*
|
|
449
|
+
* All functions take a progress value t (0..1) and return the eased value.
|
|
450
|
+
*/
|
|
451
|
+
const Easing = {
|
|
452
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
453
|
+
easeInCubic: (t) => t * t * t,
|
|
454
|
+
easeOutCubic: (t) => --t * t * t + 1,
|
|
455
|
+
easeOutBack: (t) => {
|
|
456
|
+
const c1 = 1.70158;
|
|
457
|
+
const c3 = c1 + 1;
|
|
458
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
459
|
+
}};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Lightweight tween system integrated with PixiJS Ticker.
|
|
463
|
+
* Zero external dependencies — no GSAP required.
|
|
464
|
+
*
|
|
465
|
+
* All tweens return a Promise that resolves on completion.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```ts
|
|
469
|
+
* // Fade in a sprite
|
|
470
|
+
* await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
|
|
471
|
+
*
|
|
472
|
+
* // Move and wait
|
|
473
|
+
* await Tween.to(sprite, { x: 500 }, 300);
|
|
474
|
+
*
|
|
475
|
+
* // From a starting value
|
|
476
|
+
* await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
class Tween {
|
|
480
|
+
static _tweens = [];
|
|
481
|
+
static _tickerAdded = false;
|
|
482
|
+
/**
|
|
483
|
+
* Animate properties from current values to target values.
|
|
484
|
+
*
|
|
485
|
+
* @param target - Object to animate (Sprite, Container, etc.)
|
|
486
|
+
* @param props - Target property values
|
|
487
|
+
* @param duration - Duration in milliseconds
|
|
488
|
+
* @param easing - Easing function (default: easeOutQuad)
|
|
489
|
+
* @param onUpdate - Progress callback (0..1)
|
|
490
|
+
*/
|
|
491
|
+
static to(target, props, duration, easing, onUpdate) {
|
|
492
|
+
return new Promise((resolve) => {
|
|
493
|
+
// Capture starting values
|
|
494
|
+
const from = {};
|
|
495
|
+
for (const key of Object.keys(props)) {
|
|
496
|
+
from[key] = Tween.getProperty(target, key);
|
|
497
|
+
}
|
|
498
|
+
const tween = {
|
|
499
|
+
target,
|
|
500
|
+
from,
|
|
501
|
+
to: { ...props },
|
|
502
|
+
duration: Math.max(1, duration),
|
|
503
|
+
easing: easing ?? Easing.easeOutQuad,
|
|
504
|
+
elapsed: 0,
|
|
505
|
+
delay: 0,
|
|
506
|
+
resolve,
|
|
507
|
+
onUpdate,
|
|
508
|
+
};
|
|
509
|
+
Tween._tweens.push(tween);
|
|
510
|
+
Tween.ensureTicker();
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Animate properties from given values to current values.
|
|
515
|
+
*/
|
|
516
|
+
static from(target, props, duration, easing, onUpdate) {
|
|
517
|
+
// Capture current values as "to"
|
|
518
|
+
const to = {};
|
|
519
|
+
for (const key of Object.keys(props)) {
|
|
520
|
+
to[key] = Tween.getProperty(target, key);
|
|
521
|
+
Tween.setProperty(target, key, props[key]);
|
|
522
|
+
}
|
|
523
|
+
return Tween.to(target, to, duration, easing, onUpdate);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Animate from one set of values to another.
|
|
527
|
+
*/
|
|
528
|
+
static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
|
|
529
|
+
// Set starting values
|
|
530
|
+
for (const key of Object.keys(fromProps)) {
|
|
531
|
+
Tween.setProperty(target, key, fromProps[key]);
|
|
532
|
+
}
|
|
533
|
+
return Tween.to(target, toProps, duration, easing, onUpdate);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Wait for a given duration (useful in timelines).
|
|
537
|
+
* Uses PixiJS Ticker for consistent timing with other tweens.
|
|
538
|
+
*/
|
|
539
|
+
static delay(ms) {
|
|
540
|
+
return new Promise((resolve) => {
|
|
541
|
+
let elapsed = 0;
|
|
542
|
+
const onTick = (ticker) => {
|
|
543
|
+
elapsed += ticker.deltaMS;
|
|
544
|
+
if (elapsed >= ms) {
|
|
545
|
+
Ticker.shared.remove(onTick);
|
|
546
|
+
resolve();
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
Ticker.shared.add(onTick);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Kill all tweens on a target.
|
|
554
|
+
*/
|
|
555
|
+
static killTweensOf(target) {
|
|
556
|
+
Tween._tweens = Tween._tweens.filter((tw) => {
|
|
557
|
+
if (tw.target === target) {
|
|
558
|
+
tw.resolve();
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Kill all active tweens.
|
|
566
|
+
*/
|
|
567
|
+
static killAll() {
|
|
568
|
+
for (const tw of Tween._tweens) {
|
|
569
|
+
tw.resolve();
|
|
570
|
+
}
|
|
571
|
+
Tween._tweens.length = 0;
|
|
572
|
+
}
|
|
573
|
+
/** Number of active tweens */
|
|
574
|
+
static get activeTweens() {
|
|
575
|
+
return Tween._tweens.length;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Reset the tween system — kill all tweens and remove the ticker.
|
|
579
|
+
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
580
|
+
*/
|
|
581
|
+
static reset() {
|
|
582
|
+
for (const tw of Tween._tweens) {
|
|
583
|
+
tw.resolve();
|
|
584
|
+
}
|
|
585
|
+
Tween._tweens.length = 0;
|
|
586
|
+
if (Tween._tickerAdded) {
|
|
587
|
+
Ticker.shared.remove(Tween.tick);
|
|
588
|
+
Tween._tickerAdded = false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// ─── Internal ──────────────────────────────────────────
|
|
592
|
+
static ensureTicker() {
|
|
593
|
+
if (Tween._tickerAdded)
|
|
594
|
+
return;
|
|
595
|
+
Tween._tickerAdded = true;
|
|
596
|
+
Ticker.shared.add(Tween.tick);
|
|
597
|
+
}
|
|
598
|
+
static tick = (ticker) => {
|
|
599
|
+
const dt = ticker.deltaMS;
|
|
600
|
+
const completed = [];
|
|
601
|
+
for (const tw of Tween._tweens) {
|
|
602
|
+
tw.elapsed += dt;
|
|
603
|
+
if (tw.elapsed < tw.delay)
|
|
604
|
+
continue;
|
|
605
|
+
const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
|
|
606
|
+
const t = tw.easing(raw);
|
|
607
|
+
// Interpolate each property
|
|
608
|
+
for (const key of Object.keys(tw.to)) {
|
|
609
|
+
const start = tw.from[key];
|
|
610
|
+
const end = tw.to[key];
|
|
611
|
+
const value = start + (end - start) * t;
|
|
612
|
+
Tween.setProperty(tw.target, key, value);
|
|
613
|
+
}
|
|
614
|
+
tw.onUpdate?.(raw);
|
|
615
|
+
if (raw >= 1) {
|
|
616
|
+
completed.push(tw);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Remove completed tweens
|
|
620
|
+
for (const tw of completed) {
|
|
621
|
+
const idx = Tween._tweens.indexOf(tw);
|
|
622
|
+
if (idx !== -1)
|
|
623
|
+
Tween._tweens.splice(idx, 1);
|
|
624
|
+
tw.resolve();
|
|
625
|
+
}
|
|
626
|
+
// Remove ticker when no active tweens
|
|
627
|
+
if (Tween._tweens.length === 0 && Tween._tickerAdded) {
|
|
628
|
+
Ticker.shared.remove(Tween.tick);
|
|
629
|
+
Tween._tickerAdded = false;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
|
|
634
|
+
*/
|
|
635
|
+
static getProperty(target, key) {
|
|
636
|
+
const parts = key.split('.');
|
|
637
|
+
let obj = target;
|
|
638
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
639
|
+
obj = obj[parts[i]];
|
|
640
|
+
}
|
|
641
|
+
return obj[parts[parts.length - 1]] ?? 0;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Set a potentially nested property.
|
|
645
|
+
*/
|
|
646
|
+
static setProperty(target, key, value) {
|
|
647
|
+
const parts = key.split('.');
|
|
648
|
+
let obj = target;
|
|
649
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
650
|
+
obj = obj[parts[i]];
|
|
651
|
+
}
|
|
652
|
+
obj[parts[parts.length - 1]] = value;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
5
655
|
|
|
6
656
|
const DEFAULT_COLORS = {
|
|
7
657
|
default: 0xffd700,
|
|
@@ -11,33 +661,55 @@ const DEFAULT_COLORS = {
|
|
|
11
661
|
};
|
|
12
662
|
function makeGraphicsView(w, h, radius, color) {
|
|
13
663
|
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 });
|
|
664
|
+
g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
|
|
17
665
|
return g;
|
|
18
666
|
}
|
|
19
667
|
/**
|
|
20
|
-
* Interactive button
|
|
668
|
+
* Interactive button with per-state custom views and animations.
|
|
21
669
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
670
|
+
* Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
|
|
671
|
+
* (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
|
|
672
|
+
* Falls back to colored Graphics when no custom view is provided.
|
|
24
673
|
*
|
|
25
674
|
* @example
|
|
26
675
|
* ```ts
|
|
676
|
+
* // Graphics-based (quick prototyping)
|
|
27
677
|
* const btn = new Button({
|
|
28
678
|
* width: 200, height: 60, borderRadius: 12,
|
|
29
679
|
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
30
680
|
* text: 'SPIN',
|
|
681
|
+
* onPress: () => spin(),
|
|
31
682
|
* });
|
|
32
683
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
684
|
+
* // Asset-based (production art)
|
|
685
|
+
* const btn = new Button({
|
|
686
|
+
* defaultView: 'btn-idle',
|
|
687
|
+
* hoverView: 'btn-hover',
|
|
688
|
+
* pressedView: 'btn-pressed',
|
|
689
|
+
* disabledView: 'btn-disabled',
|
|
690
|
+
* text: 'SPIN',
|
|
691
|
+
* onPress: () => spin(),
|
|
692
|
+
* });
|
|
693
|
+
*
|
|
694
|
+
* // Custom Container view
|
|
695
|
+
* const btn = new Button({
|
|
696
|
+
* defaultView: myAnimatedSprite,
|
|
697
|
+
* text: 'SPIN',
|
|
698
|
+
* });
|
|
35
699
|
* ```
|
|
36
700
|
*/
|
|
37
|
-
class Button extends
|
|
38
|
-
|
|
701
|
+
class Button extends Container {
|
|
702
|
+
__uiComponent = true;
|
|
703
|
+
_views = new Map();
|
|
704
|
+
_state = 'default';
|
|
705
|
+
_enabled = true;
|
|
706
|
+
_config;
|
|
707
|
+
_textObj = null;
|
|
708
|
+
/** Press callback */
|
|
709
|
+
onPress;
|
|
39
710
|
constructor(config = {}) {
|
|
40
|
-
|
|
711
|
+
super();
|
|
712
|
+
this._config = {
|
|
41
713
|
width: config.width ?? 200,
|
|
42
714
|
height: config.height ?? 60,
|
|
43
715
|
borderRadius: config.borderRadius ?? 8,
|
|
@@ -45,81 +717,202 @@ class Button extends FancyButton {
|
|
|
45
717
|
animationDuration: config.animationDuration ?? 100,
|
|
46
718
|
...config,
|
|
47
719
|
};
|
|
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
|
-
}
|
|
720
|
+
this.onPress = config.onPress;
|
|
721
|
+
this._buildViews(config);
|
|
82
722
|
// Text
|
|
83
723
|
if (config.text) {
|
|
84
|
-
|
|
724
|
+
this._textObj = new Text({
|
|
725
|
+
text: config.text,
|
|
726
|
+
style: {
|
|
727
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
728
|
+
fontSize: 20,
|
|
729
|
+
fill: 0xffffff,
|
|
730
|
+
fontWeight: 'bold',
|
|
731
|
+
...config.textStyle,
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
this._textObj.anchor.set(0.5);
|
|
735
|
+
this.addChild(this._textObj);
|
|
85
736
|
}
|
|
86
|
-
|
|
87
|
-
this.
|
|
737
|
+
// Interaction
|
|
738
|
+
this.eventMode = 'static';
|
|
739
|
+
this.cursor = 'pointer';
|
|
740
|
+
this.on('pointerover', this._onPointerOver, this);
|
|
741
|
+
this.on('pointerout', this._onPointerOut, this);
|
|
742
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
743
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
744
|
+
this.on('pointerupoutside', this._onPointerUpOutside, this);
|
|
88
745
|
if (config.disabled) {
|
|
89
746
|
this.enabled = false;
|
|
90
747
|
}
|
|
91
748
|
}
|
|
749
|
+
/** Current button state */
|
|
750
|
+
get state() {
|
|
751
|
+
return this._state;
|
|
752
|
+
}
|
|
92
753
|
/** Enable the button */
|
|
93
754
|
enable() {
|
|
94
755
|
this.enabled = true;
|
|
95
756
|
}
|
|
96
|
-
/** Disable the button */
|
|
97
|
-
disable() {
|
|
98
|
-
this.enabled = false;
|
|
757
|
+
/** Disable the button */
|
|
758
|
+
disable() {
|
|
759
|
+
this.enabled = false;
|
|
760
|
+
}
|
|
761
|
+
/** Whether the button is enabled */
|
|
762
|
+
get enabled() {
|
|
763
|
+
return this._enabled;
|
|
764
|
+
}
|
|
765
|
+
set enabled(value) {
|
|
766
|
+
this._enabled = value;
|
|
767
|
+
this.cursor = value ? 'pointer' : 'default';
|
|
768
|
+
this.eventMode = value ? 'static' : 'none';
|
|
769
|
+
this._setState(value ? 'default' : 'disabled');
|
|
770
|
+
}
|
|
771
|
+
/** Whether the button is disabled */
|
|
772
|
+
get disabled() {
|
|
773
|
+
return !this._enabled;
|
|
774
|
+
}
|
|
775
|
+
/** Update button text */
|
|
776
|
+
set text(value) {
|
|
777
|
+
if (this._textObj) {
|
|
778
|
+
this._textObj.text = value;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// ─── View building ──────────────────────────────────
|
|
782
|
+
_buildViews(config) {
|
|
783
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
784
|
+
const { width, height, borderRadius } = this._config;
|
|
785
|
+
const stateViews = {
|
|
786
|
+
default: config.defaultView,
|
|
787
|
+
hover: config.hoverView,
|
|
788
|
+
pressed: config.pressedView,
|
|
789
|
+
disabled: config.disabledView,
|
|
790
|
+
};
|
|
791
|
+
const states = ['default', 'hover', 'pressed', 'disabled'];
|
|
792
|
+
for (const state of states) {
|
|
793
|
+
const customView = resolveView(stateViews[state]);
|
|
794
|
+
const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
|
|
795
|
+
view.visible = state === 'default';
|
|
796
|
+
this._views.set(state, view);
|
|
797
|
+
this.addChild(view);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
_rebuildViews() {
|
|
801
|
+
for (const [, view] of this._views) {
|
|
802
|
+
this.removeChild(view);
|
|
803
|
+
view.destroy();
|
|
804
|
+
}
|
|
805
|
+
this._views.clear();
|
|
806
|
+
this._buildViews(this._config);
|
|
807
|
+
// Re-insert views before text
|
|
808
|
+
if (this._textObj && this._textObj.parent === this) {
|
|
809
|
+
this.setChildIndex(this._textObj, this.children.length - 1);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// ─── State management ───────────────────────────────
|
|
813
|
+
_setState(state) {
|
|
814
|
+
if (this._state === state)
|
|
815
|
+
return;
|
|
816
|
+
this._state = state;
|
|
817
|
+
for (const [s, view] of this._views) {
|
|
818
|
+
view.visible = s === state;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
_onPointerOver() {
|
|
822
|
+
if (!this._enabled)
|
|
823
|
+
return;
|
|
824
|
+
this._setState('hover');
|
|
825
|
+
Tween.killTweensOf(this);
|
|
826
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
827
|
+
}
|
|
828
|
+
_onPointerOut() {
|
|
829
|
+
if (!this._enabled)
|
|
830
|
+
return;
|
|
831
|
+
this._setState('default');
|
|
832
|
+
Tween.killTweensOf(this);
|
|
833
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
834
|
+
}
|
|
835
|
+
_onPointerDown() {
|
|
836
|
+
if (!this._enabled)
|
|
837
|
+
return;
|
|
838
|
+
this._setState('pressed');
|
|
839
|
+
Tween.killTweensOf(this);
|
|
840
|
+
const s = this._config.pressScale;
|
|
841
|
+
Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
842
|
+
}
|
|
843
|
+
_onPointerUp() {
|
|
844
|
+
if (!this._enabled)
|
|
845
|
+
return;
|
|
846
|
+
this._setState('hover');
|
|
847
|
+
Tween.killTweensOf(this);
|
|
848
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
849
|
+
this.onPress?.();
|
|
850
|
+
}
|
|
851
|
+
_onPointerUpOutside() {
|
|
852
|
+
if (!this._enabled)
|
|
853
|
+
return;
|
|
854
|
+
this._setState('default');
|
|
855
|
+
Tween.killTweensOf(this);
|
|
856
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
857
|
+
}
|
|
858
|
+
/** React reconciler update hook */
|
|
859
|
+
updateConfig(changed) {
|
|
860
|
+
if ('text' in changed && this._textObj)
|
|
861
|
+
this._textObj.text = changed.text;
|
|
862
|
+
if ('disabled' in changed)
|
|
863
|
+
this.enabled = !changed.disabled;
|
|
864
|
+
if ('onPress' in changed)
|
|
865
|
+
this.onPress = changed.onPress;
|
|
866
|
+
const structural = [
|
|
867
|
+
'colors', 'width', 'height', 'borderRadius', 'textStyle',
|
|
868
|
+
'defaultView', 'hoverView', 'pressedView', 'disabledView',
|
|
869
|
+
];
|
|
870
|
+
const needsRebuild = structural.some((k) => k in changed);
|
|
871
|
+
if (needsRebuild) {
|
|
872
|
+
Object.assign(this._config, changed);
|
|
873
|
+
this._rebuildViews();
|
|
874
|
+
}
|
|
99
875
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
876
|
+
destroy(options) {
|
|
877
|
+
Tween.killTweensOf(this);
|
|
878
|
+
this.off('pointerover', this._onPointerOver, this);
|
|
879
|
+
this.off('pointerout', this._onPointerOut, this);
|
|
880
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
881
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
882
|
+
this.off('pointerupoutside', this._onPointerUpOutside, this);
|
|
883
|
+
this._views.clear();
|
|
884
|
+
this._textObj = null;
|
|
885
|
+
super.destroy(options);
|
|
103
886
|
}
|
|
104
887
|
}
|
|
105
888
|
|
|
106
|
-
function makeBarGraphics(w, h, radius, color) {
|
|
107
|
-
return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
108
|
-
}
|
|
109
889
|
/**
|
|
110
|
-
* Horizontal progress bar
|
|
890
|
+
* Horizontal progress bar with optional custom track/fill views.
|
|
111
891
|
*
|
|
112
|
-
*
|
|
892
|
+
* Supports asset-based skinning: provide `trackView` and/or `fillView`
|
|
893
|
+
* as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
|
|
894
|
+
* Falls back to colored Graphics when no custom views are provided.
|
|
113
895
|
*
|
|
114
896
|
* @example
|
|
115
897
|
* ```ts
|
|
898
|
+
* // Graphics-based (quick prototyping)
|
|
116
899
|
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
117
|
-
*
|
|
118
|
-
*
|
|
900
|
+
* bar.progress = 0.5;
|
|
901
|
+
*
|
|
902
|
+
* // Asset-based (production art)
|
|
903
|
+
* const bar = new ProgressBar({
|
|
904
|
+
* width: 300, height: 20,
|
|
905
|
+
* trackView: 'bar-track',
|
|
906
|
+
* fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
|
|
907
|
+
* });
|
|
908
|
+
* bar.progress = 0.75;
|
|
119
909
|
* ```
|
|
120
910
|
*/
|
|
121
911
|
class ProgressBar extends Container {
|
|
122
|
-
|
|
912
|
+
__uiComponent = true;
|
|
913
|
+
_track;
|
|
914
|
+
_fill;
|
|
915
|
+
_fillMask;
|
|
123
916
|
_borderGfx;
|
|
124
917
|
_config;
|
|
125
918
|
_progress = 0;
|
|
@@ -138,21 +931,39 @@ class ProgressBar extends Container {
|
|
|
138
931
|
animationSpeed: config.animationSpeed ?? 0.1,
|
|
139
932
|
};
|
|
140
933
|
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
|
-
|
|
934
|
+
// Track background — custom view or Graphics
|
|
935
|
+
const customTrack = resolveView(config.trackView);
|
|
936
|
+
if (customTrack) {
|
|
937
|
+
customTrack.width = width;
|
|
938
|
+
customTrack.height = height;
|
|
939
|
+
this._track = customTrack;
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
const g = new Graphics();
|
|
943
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
944
|
+
this._track = g;
|
|
945
|
+
}
|
|
946
|
+
this.addChild(this._track);
|
|
947
|
+
// Fill bar — custom view or Graphics
|
|
948
|
+
const customFill = resolveView(config.fillView);
|
|
949
|
+
if (customFill) {
|
|
950
|
+
customFill.x = borderWidth;
|
|
951
|
+
customFill.y = borderWidth;
|
|
952
|
+
customFill.width = width - borderWidth * 2;
|
|
953
|
+
customFill.height = height - borderWidth * 2;
|
|
954
|
+
this._fill = customFill;
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
const g = new Graphics();
|
|
958
|
+
g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
|
|
959
|
+
this._fill = g;
|
|
960
|
+
}
|
|
961
|
+
this.addChild(this._fill);
|
|
962
|
+
// Mask for the fill (controls visible width)
|
|
963
|
+
this._fillMask = new Graphics();
|
|
964
|
+
this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
|
|
965
|
+
this.addChild(this._fillMask);
|
|
966
|
+
this._fill.mask = this._fillMask;
|
|
156
967
|
// Border overlay
|
|
157
968
|
this._borderGfx = new Graphics();
|
|
158
969
|
if (borderColor !== undefined && borderWidth > 0) {
|
|
@@ -170,7 +981,7 @@ class ProgressBar extends Container {
|
|
|
170
981
|
this._progress = Math.max(0, Math.min(1, value));
|
|
171
982
|
if (!this._config.animated) {
|
|
172
983
|
this._displayedProgress = this._progress;
|
|
173
|
-
this.
|
|
984
|
+
this.updateMask();
|
|
174
985
|
}
|
|
175
986
|
}
|
|
176
987
|
/**
|
|
@@ -181,11 +992,26 @@ class ProgressBar extends Container {
|
|
|
181
992
|
return;
|
|
182
993
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
183
994
|
this._displayedProgress = this._progress;
|
|
995
|
+
this.updateMask();
|
|
184
996
|
return;
|
|
185
997
|
}
|
|
186
998
|
this._displayedProgress +=
|
|
187
999
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
188
|
-
this.
|
|
1000
|
+
this.updateMask();
|
|
1001
|
+
}
|
|
1002
|
+
/** React reconciler update hook */
|
|
1003
|
+
updateConfig(changed) {
|
|
1004
|
+
if ('progress' in changed)
|
|
1005
|
+
this.progress = changed.progress;
|
|
1006
|
+
if ('animated' in changed)
|
|
1007
|
+
this._config.animated = changed.animated;
|
|
1008
|
+
if ('animationSpeed' in changed)
|
|
1009
|
+
this._config.animationSpeed = changed.animationSpeed;
|
|
1010
|
+
}
|
|
1011
|
+
updateMask() {
|
|
1012
|
+
const w = this._config.width * this._displayedProgress;
|
|
1013
|
+
this._fillMask.clear();
|
|
1014
|
+
this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
|
|
189
1015
|
}
|
|
190
1016
|
}
|
|
191
1017
|
|
|
@@ -203,6 +1029,7 @@ class ProgressBar extends Container {
|
|
|
203
1029
|
* ```
|
|
204
1030
|
*/
|
|
205
1031
|
class Label extends Container {
|
|
1032
|
+
__uiComponent = true;
|
|
206
1033
|
_text;
|
|
207
1034
|
_maxWidth;
|
|
208
1035
|
_autoFit;
|
|
@@ -269,6 +1096,21 @@ class Label extends Container {
|
|
|
269
1096
|
maximumFractionDigits: decimals,
|
|
270
1097
|
}).format(value);
|
|
271
1098
|
}
|
|
1099
|
+
/** React reconciler update hook */
|
|
1100
|
+
updateConfig(changed) {
|
|
1101
|
+
if ('text' in changed)
|
|
1102
|
+
this.text = changed.text;
|
|
1103
|
+
if ('maxWidth' in changed)
|
|
1104
|
+
this.maxWidth = changed.maxWidth;
|
|
1105
|
+
if ('autoFit' in changed) {
|
|
1106
|
+
this._autoFit = changed.autoFit;
|
|
1107
|
+
this.fitText();
|
|
1108
|
+
}
|
|
1109
|
+
if ('style' in changed && typeof changed.style === 'object') {
|
|
1110
|
+
Object.assign(this._text.style, changed.style);
|
|
1111
|
+
this.fitText();
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
272
1114
|
fitText() {
|
|
273
1115
|
if (!this._autoFit || this._maxWidth === Infinity)
|
|
274
1116
|
return;
|
|
@@ -281,10 +1123,10 @@ class Label extends Container {
|
|
|
281
1123
|
}
|
|
282
1124
|
|
|
283
1125
|
/**
|
|
284
|
-
* Background panel
|
|
1126
|
+
* Background panel with optional flexbox content layout.
|
|
285
1127
|
*
|
|
286
1128
|
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
287
|
-
* Children added
|
|
1129
|
+
* Children added via `addContent()` participate in flex layout automatically.
|
|
288
1130
|
*
|
|
289
1131
|
* @example
|
|
290
1132
|
* ```ts
|
|
@@ -299,9 +1141,14 @@ class Label extends Container {
|
|
|
299
1141
|
* });
|
|
300
1142
|
* ```
|
|
301
1143
|
*/
|
|
302
|
-
class Panel extends
|
|
1144
|
+
class Panel extends Container {
|
|
1145
|
+
__uiComponent = true;
|
|
1146
|
+
_bg;
|
|
1147
|
+
_content;
|
|
1148
|
+
_internalSetup = true;
|
|
303
1149
|
_panelConfig;
|
|
304
1150
|
constructor(config = {}) {
|
|
1151
|
+
super();
|
|
305
1152
|
const resolvedConfig = {
|
|
306
1153
|
width: config.width ?? 400,
|
|
307
1154
|
height: config.height ?? 300,
|
|
@@ -309,8 +1156,8 @@ class Panel extends LayoutContainer {
|
|
|
309
1156
|
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
310
1157
|
...config,
|
|
311
1158
|
};
|
|
312
|
-
|
|
313
|
-
|
|
1159
|
+
this._panelConfig = resolvedConfig;
|
|
1160
|
+
// Create background
|
|
314
1161
|
if (config.nineSliceTexture) {
|
|
315
1162
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
316
1163
|
? Texture.from(config.nineSliceTexture)
|
|
@@ -326,126 +1173,110 @@ class Panel extends LayoutContainer {
|
|
|
326
1173
|
nineSlice.width = resolvedConfig.width;
|
|
327
1174
|
nineSlice.height = resolvedConfig.height;
|
|
328
1175
|
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
329
|
-
|
|
1176
|
+
this._bg = nineSlice;
|
|
330
1177
|
}
|
|
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;
|
|
1178
|
+
else {
|
|
1179
|
+
const g = new Graphics();
|
|
1180
|
+
const bgColor = config.backgroundColor ?? 0x1a1a2e;
|
|
1181
|
+
const radius = config.borderRadius ?? 0;
|
|
1182
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
|
|
344
1183
|
if (config.borderColor !== undefined && config.borderWidth) {
|
|
345
|
-
|
|
346
|
-
|
|
1184
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
|
|
1185
|
+
.stroke({ color: config.borderColor, width: config.borderWidth });
|
|
347
1186
|
}
|
|
1187
|
+
g.alpha = resolvedConfig.backgroundAlpha;
|
|
1188
|
+
this._bg = g;
|
|
348
1189
|
}
|
|
349
|
-
this.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1190
|
+
this.addChild(this._bg);
|
|
1191
|
+
// Create content flex container
|
|
1192
|
+
this._content = new FlexContainer({
|
|
1193
|
+
...config.layout,
|
|
1194
|
+
direction: config.layout?.direction ?? 'column',
|
|
1195
|
+
justifyContent: config.layout?.justifyContent ?? 'start',
|
|
1196
|
+
alignItems: config.layout?.alignItems ?? 'start',
|
|
1197
|
+
gap: config.layout?.gap ?? 0,
|
|
1198
|
+
padding: resolvedConfig.padding,
|
|
1199
|
+
width: resolvedConfig.width,
|
|
1200
|
+
height: resolvedConfig.height,
|
|
1201
|
+
});
|
|
1202
|
+
this.addChild(this._content);
|
|
1203
|
+
this._internalSetup = false;
|
|
353
1204
|
}
|
|
354
|
-
/** Access the content container
|
|
1205
|
+
/** Access the content flex container — add children here for layout */
|
|
355
1206
|
get content() {
|
|
356
|
-
return this.
|
|
1207
|
+
return this._content;
|
|
1208
|
+
}
|
|
1209
|
+
/** Convenience: add a child to the content layout */
|
|
1210
|
+
addContent(child) {
|
|
1211
|
+
this._content.addFlexChild(child);
|
|
1212
|
+
this._content.updateLayout();
|
|
1213
|
+
return this;
|
|
357
1214
|
}
|
|
358
1215
|
/** Resize the panel */
|
|
359
1216
|
setSize(width, height) {
|
|
360
1217
|
this._panelConfig.width = width;
|
|
361
1218
|
this._panelConfig.height = height;
|
|
362
|
-
|
|
1219
|
+
// Resize background
|
|
1220
|
+
if (this._bg instanceof NineSliceSprite) {
|
|
1221
|
+
this._bg.width = width;
|
|
1222
|
+
this._bg.height = height;
|
|
1223
|
+
}
|
|
1224
|
+
else if (this._bg instanceof Graphics) {
|
|
1225
|
+
const radius = this._panelConfig.borderRadius ?? 0;
|
|
1226
|
+
const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
|
|
1227
|
+
this._bg.clear();
|
|
1228
|
+
this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
|
|
1229
|
+
if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
|
|
1230
|
+
this._bg.roundRect(0, 0, width, height, radius)
|
|
1231
|
+
.stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
|
|
1232
|
+
}
|
|
1233
|
+
this._bg.alpha = this._panelConfig.backgroundAlpha;
|
|
1234
|
+
}
|
|
1235
|
+
this._content.resize(width, height);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Override addChild so external children are routed to content FlexContainer.
|
|
1239
|
+
* Enables `<panel><label /><button /></panel>` in React JSX.
|
|
1240
|
+
*/
|
|
1241
|
+
addChild(...children) {
|
|
1242
|
+
if (this._internalSetup) {
|
|
1243
|
+
return super.addChild(...children);
|
|
1244
|
+
}
|
|
1245
|
+
for (const child of children) {
|
|
1246
|
+
this._content.addFlexChild(child);
|
|
1247
|
+
}
|
|
1248
|
+
this._content.updateLayout();
|
|
1249
|
+
return children[0];
|
|
1250
|
+
}
|
|
1251
|
+
removeChild(...children) {
|
|
1252
|
+
if (this._internalSetup) {
|
|
1253
|
+
return super.removeChild(...children);
|
|
1254
|
+
}
|
|
1255
|
+
for (const child of children) {
|
|
1256
|
+
this._content.removeFlexChild(child);
|
|
1257
|
+
}
|
|
1258
|
+
return children[0];
|
|
1259
|
+
}
|
|
1260
|
+
/** React reconciler update hook */
|
|
1261
|
+
updateConfig(changed) {
|
|
1262
|
+
if ('width' in changed || 'height' in changed) {
|
|
1263
|
+
this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
|
|
1264
|
+
}
|
|
1265
|
+
if ('backgroundAlpha' in changed) {
|
|
1266
|
+
this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
|
|
1267
|
+
this._bg.alpha = changed.backgroundAlpha;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
destroy(options) {
|
|
1271
|
+
super.destroy(options);
|
|
363
1272
|
}
|
|
364
1273
|
}
|
|
365
1274
|
|
|
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
1275
|
/**
|
|
445
1276
|
* Reactive balance display component.
|
|
446
1277
|
*
|
|
447
1278
|
* Automatically formats currency and can animate value changes
|
|
448
|
-
* with a smooth countup/countdown effect.
|
|
1279
|
+
* with a smooth countup/countdown effect using engine Tween.
|
|
449
1280
|
*
|
|
450
1281
|
* @example
|
|
451
1282
|
* ```ts
|
|
@@ -457,13 +1288,14 @@ const Easing = {
|
|
|
457
1288
|
* ```
|
|
458
1289
|
*/
|
|
459
1290
|
class BalanceDisplay extends Container {
|
|
1291
|
+
__uiComponent = true;
|
|
460
1292
|
_prefixLabel = null;
|
|
461
1293
|
_valueLabel;
|
|
462
1294
|
_config;
|
|
463
1295
|
_currentValue = 0;
|
|
464
1296
|
_displayedValue = 0;
|
|
465
|
-
|
|
466
|
-
|
|
1297
|
+
/** Internal target for Tween animation */
|
|
1298
|
+
_tweenTarget = { value: 0 };
|
|
467
1299
|
constructor(config = {}) {
|
|
468
1300
|
super();
|
|
469
1301
|
this._config = {
|
|
@@ -524,37 +1356,13 @@ class BalanceDisplay extends Container {
|
|
|
524
1356
|
this._config.currency = currency;
|
|
525
1357
|
this.updateDisplay();
|
|
526
1358
|
}
|
|
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);
|
|
1359
|
+
animateValue(from, to) {
|
|
1360
|
+
// Cancel any running animation
|
|
1361
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1362
|
+
this._tweenTarget.value = from;
|
|
1363
|
+
Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
|
|
1364
|
+
this._displayedValue = this._tweenTarget.value;
|
|
1365
|
+
this.updateDisplay();
|
|
558
1366
|
});
|
|
559
1367
|
}
|
|
560
1368
|
updateDisplay() {
|
|
@@ -566,13 +1374,24 @@ class BalanceDisplay extends Container {
|
|
|
566
1374
|
this._valueLabel.y = 14;
|
|
567
1375
|
}
|
|
568
1376
|
}
|
|
1377
|
+
/** React reconciler update hook */
|
|
1378
|
+
updateConfig(changed) {
|
|
1379
|
+
if ('value' in changed)
|
|
1380
|
+
this.setValue(changed.value);
|
|
1381
|
+
if ('currency' in changed)
|
|
1382
|
+
this.setCurrency(changed.currency);
|
|
1383
|
+
}
|
|
1384
|
+
destroy(options) {
|
|
1385
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1386
|
+
super.destroy(options);
|
|
1387
|
+
}
|
|
569
1388
|
}
|
|
570
1389
|
|
|
571
1390
|
/**
|
|
572
1391
|
* Win amount display with countup animation.
|
|
573
1392
|
*
|
|
574
1393
|
* Shows a dramatic countup from 0 to the win amount, with optional
|
|
575
|
-
* scale pop effect — typical of slot games.
|
|
1394
|
+
* scale pop effect — typical of slot games. Uses engine Tween system.
|
|
576
1395
|
*
|
|
577
1396
|
* @example
|
|
578
1397
|
* ```ts
|
|
@@ -583,9 +1402,11 @@ class BalanceDisplay extends Container {
|
|
|
583
1402
|
* ```
|
|
584
1403
|
*/
|
|
585
1404
|
class WinDisplay extends Container {
|
|
1405
|
+
__uiComponent = true;
|
|
586
1406
|
_label;
|
|
587
1407
|
_config;
|
|
588
|
-
|
|
1408
|
+
/** Internal target for Tween countup */
|
|
1409
|
+
_tweenTarget = { value: 0 };
|
|
589
1410
|
constructor(config = {}) {
|
|
590
1411
|
super();
|
|
591
1412
|
this._config = {
|
|
@@ -611,258 +1432,60 @@ class WinDisplay extends Container {
|
|
|
611
1432
|
* Show a win with countup animation.
|
|
612
1433
|
*
|
|
613
1434
|
* @param amount - Win amount
|
|
614
|
-
* @returns Promise that resolves when the animation completes
|
|
615
|
-
*/
|
|
616
|
-
async showWin(amount) {
|
|
617
|
-
this.visible = true;
|
|
618
|
-
this.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
this.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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);
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Kill all tweens on a target.
|
|
767
|
-
*/
|
|
768
|
-
static killTweensOf(target) {
|
|
769
|
-
Tween._tweens = Tween._tweens.filter((tw) => {
|
|
770
|
-
if (tw.target === target) {
|
|
771
|
-
tw.resolve();
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
return true;
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Kill all active tweens.
|
|
779
|
-
*/
|
|
780
|
-
static killAll() {
|
|
781
|
-
for (const tw of Tween._tweens) {
|
|
782
|
-
tw.resolve();
|
|
783
|
-
}
|
|
784
|
-
Tween._tweens.length = 0;
|
|
785
|
-
}
|
|
786
|
-
/** Number of active tweens */
|
|
787
|
-
static get activeTweens() {
|
|
788
|
-
return Tween._tweens.length;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Reset the tween system — kill all tweens and remove the ticker.
|
|
792
|
-
* Useful for cleanup between game instances, tests, or hot-reload.
|
|
793
|
-
*/
|
|
794
|
-
static reset() {
|
|
795
|
-
for (const tw of Tween._tweens) {
|
|
796
|
-
tw.resolve();
|
|
797
|
-
}
|
|
798
|
-
Tween._tweens.length = 0;
|
|
799
|
-
if (Tween._tickerAdded) {
|
|
800
|
-
Ticker.shared.remove(Tween.tick);
|
|
801
|
-
Tween._tickerAdded = false;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
// ─── Internal ──────────────────────────────────────────
|
|
805
|
-
static ensureTicker() {
|
|
806
|
-
if (Tween._tickerAdded)
|
|
807
|
-
return;
|
|
808
|
-
Tween._tickerAdded = true;
|
|
809
|
-
Ticker.shared.add(Tween.tick);
|
|
1435
|
+
* @returns Promise that resolves when the animation completes
|
|
1436
|
+
*/
|
|
1437
|
+
async showWin(amount) {
|
|
1438
|
+
this.visible = true;
|
|
1439
|
+
this.alpha = 1;
|
|
1440
|
+
// Cancel any running animation
|
|
1441
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1442
|
+
Tween.killTweensOf(this);
|
|
1443
|
+
// Setup countup
|
|
1444
|
+
this._tweenTarget.value = 0;
|
|
1445
|
+
this.scale.set(0.5);
|
|
1446
|
+
// Scale pop animation
|
|
1447
|
+
const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
|
|
1448
|
+
// Countup animation
|
|
1449
|
+
const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
|
|
1450
|
+
this.displayAmount(this._tweenTarget.value);
|
|
1451
|
+
});
|
|
1452
|
+
await Promise.all([scalePromise, countupPromise]);
|
|
1453
|
+
// Ensure final value is exact
|
|
1454
|
+
this.displayAmount(amount);
|
|
1455
|
+
this.scale.set(1);
|
|
810
1456
|
}
|
|
811
|
-
static tick = (ticker) => {
|
|
812
|
-
const dt = ticker.deltaMS;
|
|
813
|
-
const completed = [];
|
|
814
|
-
for (const tw of Tween._tweens) {
|
|
815
|
-
tw.elapsed += dt;
|
|
816
|
-
if (tw.elapsed < tw.delay)
|
|
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
1457
|
/**
|
|
846
|
-
*
|
|
1458
|
+
* Skip the countup animation and show the final amount immediately.
|
|
847
1459
|
*/
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
return obj[parts[parts.length - 1]] ?? 0;
|
|
1460
|
+
skipCountup(amount) {
|
|
1461
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1462
|
+
Tween.killTweensOf(this);
|
|
1463
|
+
this.displayAmount(amount);
|
|
1464
|
+
this.scale.set(1);
|
|
855
1465
|
}
|
|
856
1466
|
/**
|
|
857
|
-
*
|
|
1467
|
+
* Hide the win display.
|
|
858
1468
|
*/
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1469
|
+
hide() {
|
|
1470
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1471
|
+
Tween.killTweensOf(this);
|
|
1472
|
+
this.visible = false;
|
|
1473
|
+
this._label.text = '';
|
|
1474
|
+
}
|
|
1475
|
+
displayAmount(amount) {
|
|
1476
|
+
this._label.setCurrency(amount, this._config.currency, this._config.locale);
|
|
1477
|
+
}
|
|
1478
|
+
/** React reconciler update hook */
|
|
1479
|
+
updateConfig(changed) {
|
|
1480
|
+
if ('currency' in changed)
|
|
1481
|
+
this._config.currency = changed.currency;
|
|
1482
|
+
if ('locale' in changed)
|
|
1483
|
+
this._config.locale = changed.locale;
|
|
1484
|
+
}
|
|
1485
|
+
destroy(options) {
|
|
1486
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
1487
|
+
Tween.killTweensOf(this);
|
|
1488
|
+
super.destroy(options);
|
|
866
1489
|
}
|
|
867
1490
|
}
|
|
868
1491
|
|
|
@@ -870,7 +1493,7 @@ class Tween {
|
|
|
870
1493
|
* Modal overlay component.
|
|
871
1494
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
872
1495
|
*
|
|
873
|
-
*
|
|
1496
|
+
* Content is automatically centered via position calculations.
|
|
874
1497
|
*
|
|
875
1498
|
* @example
|
|
876
1499
|
* ```ts
|
|
@@ -881,6 +1504,7 @@ class Tween {
|
|
|
881
1504
|
* ```
|
|
882
1505
|
*/
|
|
883
1506
|
class Modal extends Container {
|
|
1507
|
+
__uiComponent = true;
|
|
884
1508
|
_overlay;
|
|
885
1509
|
_contentContainer;
|
|
886
1510
|
_config;
|
|
@@ -950,6 +1574,17 @@ class Modal extends Container {
|
|
|
950
1574
|
this._showing = false;
|
|
951
1575
|
this.onClose?.();
|
|
952
1576
|
}
|
|
1577
|
+
/** React reconciler update hook */
|
|
1578
|
+
updateConfig(changed) {
|
|
1579
|
+
if ('overlayAlpha' in changed)
|
|
1580
|
+
this._config.overlayAlpha = changed.overlayAlpha;
|
|
1581
|
+
if ('closeOnOverlay' in changed)
|
|
1582
|
+
this._config.closeOnOverlay = changed.closeOnOverlay;
|
|
1583
|
+
if ('animationDuration' in changed)
|
|
1584
|
+
this._config.animationDuration = changed.animationDuration;
|
|
1585
|
+
if ('onClose' in changed)
|
|
1586
|
+
this.onClose = changed.onClose;
|
|
1587
|
+
}
|
|
953
1588
|
}
|
|
954
1589
|
|
|
955
1590
|
const TOAST_COLORS = {
|
|
@@ -969,17 +1604,21 @@ const TOAST_COLORS = {
|
|
|
969
1604
|
* ```
|
|
970
1605
|
*/
|
|
971
1606
|
class Toast extends Container {
|
|
1607
|
+
__uiComponent = true;
|
|
972
1608
|
_bg;
|
|
1609
|
+
_customBg;
|
|
973
1610
|
_text;
|
|
974
1611
|
_config;
|
|
975
|
-
|
|
1612
|
+
_dismissPending = false;
|
|
976
1613
|
constructor(config = {}) {
|
|
977
1614
|
super();
|
|
978
1615
|
this._config = {
|
|
979
1616
|
duration: config.duration ?? 3000,
|
|
980
1617
|
bottomOffset: config.bottomOffset ?? 60,
|
|
981
1618
|
};
|
|
982
|
-
|
|
1619
|
+
const customBg = resolveView(config.backgroundView);
|
|
1620
|
+
this._customBg = !!customBg;
|
|
1621
|
+
this._bg = customBg ?? new Graphics();
|
|
983
1622
|
this.addChild(this._bg);
|
|
984
1623
|
this._text = new Text({
|
|
985
1624
|
text: '',
|
|
@@ -997,18 +1636,27 @@ class Toast extends Container {
|
|
|
997
1636
|
* Show a toast message.
|
|
998
1637
|
*/
|
|
999
1638
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1639
|
+
// Cancel any pending dismiss
|
|
1640
|
+
Tween.killTweensOf(this);
|
|
1641
|
+
this._dismissPending = false;
|
|
1003
1642
|
this._text.text = message;
|
|
1004
1643
|
const padding = 20;
|
|
1005
1644
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
1006
1645
|
const height = 44;
|
|
1007
1646
|
const radius = 8;
|
|
1008
1647
|
// Draw the background
|
|
1009
|
-
this.
|
|
1010
|
-
|
|
1011
|
-
|
|
1648
|
+
if (this._customBg) {
|
|
1649
|
+
this._bg.width = width;
|
|
1650
|
+
this._bg.height = height;
|
|
1651
|
+
this._bg.x = -width / 2;
|
|
1652
|
+
this._bg.y = -height / 2;
|
|
1653
|
+
}
|
|
1654
|
+
else {
|
|
1655
|
+
const g = this._bg;
|
|
1656
|
+
g.clear();
|
|
1657
|
+
g.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
1658
|
+
g.fill(TOAST_COLORS[type]);
|
|
1659
|
+
}
|
|
1012
1660
|
// Position
|
|
1013
1661
|
if (viewWidth && viewHeight) {
|
|
1014
1662
|
this.x = viewWidth / 2;
|
|
@@ -1019,9 +1667,12 @@ class Toast extends Container {
|
|
|
1019
1667
|
this.y += 20;
|
|
1020
1668
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
1021
1669
|
if (this._config.duration > 0) {
|
|
1022
|
-
this.
|
|
1023
|
-
|
|
1024
|
-
|
|
1670
|
+
this._dismissPending = true;
|
|
1671
|
+
await Tween.delay(this._config.duration);
|
|
1672
|
+
if (this._dismissPending) {
|
|
1673
|
+
this._dismissPending = false;
|
|
1674
|
+
await this.dismiss();
|
|
1675
|
+
}
|
|
1025
1676
|
}
|
|
1026
1677
|
}
|
|
1027
1678
|
/**
|
|
@@ -1030,57 +1681,36 @@ class Toast extends Container {
|
|
|
1030
1681
|
async dismiss() {
|
|
1031
1682
|
if (!this.visible)
|
|
1032
1683
|
return;
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
this._dismissTimeout = null;
|
|
1036
|
-
}
|
|
1684
|
+
this._dismissPending = false;
|
|
1685
|
+
Tween.killTweensOf(this);
|
|
1037
1686
|
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
1038
1687
|
this.visible = false;
|
|
1039
1688
|
}
|
|
1689
|
+
/** React reconciler update hook */
|
|
1690
|
+
updateConfig(changed) {
|
|
1691
|
+
if ('duration' in changed)
|
|
1692
|
+
this._config.duration = changed.duration;
|
|
1693
|
+
if ('bottomOffset' in changed)
|
|
1694
|
+
this._config.bottomOffset = changed.bottomOffset;
|
|
1695
|
+
}
|
|
1696
|
+
destroy(options) {
|
|
1697
|
+
this._dismissPending = false;
|
|
1698
|
+
Tween.killTweensOf(this);
|
|
1699
|
+
super.destroy(options);
|
|
1700
|
+
}
|
|
1040
1701
|
}
|
|
1041
1702
|
|
|
1042
1703
|
// ─── 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) {
|
|
1704
|
+
function directionToFlex(direction) {
|
|
1055
1705
|
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
|
-
};
|
|
1706
|
+
case 'horizontal': return { direction: 'row', wrap: false };
|
|
1707
|
+
case 'vertical': return { direction: 'column', wrap: false };
|
|
1708
|
+
case 'grid': return { direction: 'row', wrap: true };
|
|
1709
|
+
case 'wrap': return { direction: 'row', wrap: true };
|
|
1068
1710
|
}
|
|
1069
1711
|
}
|
|
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
1712
|
/**
|
|
1083
|
-
* Responsive layout container powered by
|
|
1713
|
+
* Responsive layout container powered by a lightweight built-in flex layout solver.
|
|
1084
1714
|
*
|
|
1085
1715
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
1086
1716
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -1107,6 +1737,7 @@ function buildLayoutStyles(config) {
|
|
|
1107
1737
|
* ```
|
|
1108
1738
|
*/
|
|
1109
1739
|
class Layout extends Container {
|
|
1740
|
+
__uiComponent = true;
|
|
1110
1741
|
_layoutConfig;
|
|
1111
1742
|
_padding;
|
|
1112
1743
|
_anchor;
|
|
@@ -1115,6 +1746,7 @@ class Layout extends Container {
|
|
|
1115
1746
|
_items = [];
|
|
1116
1747
|
_viewportWidth = 0;
|
|
1117
1748
|
_viewportHeight = 0;
|
|
1749
|
+
_flex;
|
|
1118
1750
|
constructor(config = {}) {
|
|
1119
1751
|
super();
|
|
1120
1752
|
this._layoutConfig = {
|
|
@@ -1124,7 +1756,7 @@ class Layout extends Container {
|
|
|
1124
1756
|
autoLayout: config.autoLayout ?? true,
|
|
1125
1757
|
columns: config.columns ?? 2,
|
|
1126
1758
|
};
|
|
1127
|
-
this._padding =
|
|
1759
|
+
this._padding = config.padding ?? 0;
|
|
1128
1760
|
this._anchor = config.anchor ?? 'top-left';
|
|
1129
1761
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
1130
1762
|
this._breakpoints = config.breakpoints
|
|
@@ -1132,14 +1764,18 @@ class Layout extends Container {
|
|
|
1132
1764
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
1133
1765
|
.sort((a, b) => a[0] - b[0])
|
|
1134
1766
|
: [];
|
|
1767
|
+
// Create internal FlexContainer
|
|
1768
|
+
this._flex = new FlexContainer();
|
|
1769
|
+
super.addChild(this._flex);
|
|
1135
1770
|
this.applyLayoutStyles();
|
|
1136
1771
|
}
|
|
1137
1772
|
/** Add an item to the layout */
|
|
1138
1773
|
addItem(child) {
|
|
1139
1774
|
this._items.push(child);
|
|
1140
|
-
this.
|
|
1141
|
-
|
|
1142
|
-
|
|
1775
|
+
const flexConfig = this.buildFlexItemConfig(child);
|
|
1776
|
+
this._flex.addFlexChild(child, flexConfig);
|
|
1777
|
+
if (this._layoutConfig.autoLayout) {
|
|
1778
|
+
this.applyLayoutStyles();
|
|
1143
1779
|
}
|
|
1144
1780
|
return this;
|
|
1145
1781
|
}
|
|
@@ -1148,15 +1784,13 @@ class Layout extends Container {
|
|
|
1148
1784
|
const idx = this._items.indexOf(child);
|
|
1149
1785
|
if (idx !== -1) {
|
|
1150
1786
|
this._items.splice(idx, 1);
|
|
1151
|
-
this.
|
|
1787
|
+
this._flex.removeFlexChild(child);
|
|
1152
1788
|
}
|
|
1153
1789
|
return this;
|
|
1154
1790
|
}
|
|
1155
1791
|
/** Remove all items */
|
|
1156
1792
|
clearItems() {
|
|
1157
|
-
|
|
1158
|
-
this.removeChild(item);
|
|
1159
|
-
}
|
|
1793
|
+
this._flex.clearFlexChildren();
|
|
1160
1794
|
this._items.length = 0;
|
|
1161
1795
|
return this;
|
|
1162
1796
|
}
|
|
@@ -1179,43 +1813,58 @@ class Layout extends Container {
|
|
|
1179
1813
|
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
1180
1814
|
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
1181
1815
|
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
1182
|
-
effective.
|
|
1183
|
-
const padding = effective.padding !== undefined
|
|
1184
|
-
? normalizePadding(effective.padding)
|
|
1185
|
-
: this._padding;
|
|
1816
|
+
const padding = effective.padding ?? this._padding;
|
|
1186
1817
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
1187
|
-
const
|
|
1188
|
-
this.
|
|
1818
|
+
const { direction: flexDir, wrap } = directionToFlex(direction);
|
|
1819
|
+
this._flex.setDirection(flexDir);
|
|
1820
|
+
this._flex.setJustifyContent('start');
|
|
1821
|
+
this._flex.setAlignItems(alignment);
|
|
1822
|
+
this._flex.setGap(gap);
|
|
1823
|
+
this._flex.setPadding(padding);
|
|
1824
|
+
// Wrap and maxWidth
|
|
1825
|
+
if (wrap) {
|
|
1826
|
+
this._flex._config.flexWrap = true;
|
|
1827
|
+
if (direction === 'grid' && maxWidth < Infinity) {
|
|
1828
|
+
this._flex._maxWidth = maxWidth;
|
|
1829
|
+
}
|
|
1830
|
+
if (maxWidth < Infinity) {
|
|
1831
|
+
this._flex._maxWidth = maxWidth;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
this._flex._config.flexWrap = false;
|
|
1836
|
+
}
|
|
1837
|
+
// Update grid child widths
|
|
1189
1838
|
if (direction === 'grid') {
|
|
1190
1839
|
for (const item of this._items) {
|
|
1191
|
-
this.
|
|
1840
|
+
const flexConfig = this.buildFlexItemConfig(item);
|
|
1841
|
+
item._flexConfig = flexConfig;
|
|
1192
1842
|
}
|
|
1193
1843
|
}
|
|
1844
|
+
// Set explicit size if we have viewport dimensions
|
|
1845
|
+
if (this._viewportWidth > 0 && this._viewportHeight > 0) {
|
|
1846
|
+
this._flex.resize(this._viewportWidth, this._viewportHeight);
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
this._flex.updateLayout();
|
|
1850
|
+
}
|
|
1194
1851
|
}
|
|
1195
|
-
|
|
1852
|
+
buildFlexItemConfig(_child) {
|
|
1196
1853
|
const effective = this.resolveConfig();
|
|
1854
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
1197
1855
|
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;
|
|
1856
|
+
if (direction === 'grid' && columns > 0) {
|
|
1857
|
+
// For grid, give each item a proportional width
|
|
1858
|
+
// The actual pixel width will be computed during layout
|
|
1859
|
+
return { flexGrow: 1 };
|
|
1210
1860
|
}
|
|
1861
|
+
return undefined;
|
|
1211
1862
|
}
|
|
1212
1863
|
applyAnchor() {
|
|
1213
1864
|
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
1214
1865
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
1215
1866
|
return;
|
|
1216
|
-
const
|
|
1217
|
-
const contentW = bounds.width * this.scale.x;
|
|
1218
|
-
const contentH = bounds.height * this.scale.y;
|
|
1867
|
+
const { width: contentW, height: contentH } = this._flex.getContentSize();
|
|
1219
1868
|
const vw = this._viewportWidth;
|
|
1220
1869
|
const vh = this._viewportHeight;
|
|
1221
1870
|
let anchorX = 0;
|
|
@@ -1238,8 +1887,8 @@ class Layout extends Container {
|
|
|
1238
1887
|
else {
|
|
1239
1888
|
anchorY = (vh - contentH) / 2;
|
|
1240
1889
|
}
|
|
1241
|
-
this.x = anchorX
|
|
1242
|
-
this.y = anchorY
|
|
1890
|
+
this.x = anchorX;
|
|
1891
|
+
this.y = anchorY;
|
|
1243
1892
|
}
|
|
1244
1893
|
resolveConfig() {
|
|
1245
1894
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
@@ -1252,18 +1901,34 @@ class Layout extends Container {
|
|
|
1252
1901
|
}
|
|
1253
1902
|
return {};
|
|
1254
1903
|
}
|
|
1904
|
+
/** React reconciler update hook */
|
|
1905
|
+
updateConfig(changed) {
|
|
1906
|
+
if ('direction' in changed)
|
|
1907
|
+
this._layoutConfig.direction = changed.direction;
|
|
1908
|
+
if ('gap' in changed)
|
|
1909
|
+
this._layoutConfig.gap = changed.gap;
|
|
1910
|
+
if ('alignment' in changed)
|
|
1911
|
+
this._layoutConfig.alignment = changed.alignment;
|
|
1912
|
+
if ('anchor' in changed)
|
|
1913
|
+
this._anchor = changed.anchor;
|
|
1914
|
+
if ('padding' in changed)
|
|
1915
|
+
this._padding = changed.padding;
|
|
1916
|
+
if ('columns' in changed)
|
|
1917
|
+
this._layoutConfig.columns = changed.columns;
|
|
1918
|
+
this.applyLayoutStyles();
|
|
1919
|
+
if (this._viewportWidth > 0)
|
|
1920
|
+
this.applyAnchor();
|
|
1921
|
+
}
|
|
1922
|
+
destroy(options) {
|
|
1923
|
+
this._items.length = 0;
|
|
1924
|
+
super.destroy(options);
|
|
1925
|
+
}
|
|
1255
1926
|
}
|
|
1256
1927
|
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
horizontal: 'horizontal',
|
|
1260
|
-
both: 'bidirectional',
|
|
1261
|
-
};
|
|
1928
|
+
const DECELERATION = 0.95;
|
|
1929
|
+
const MIN_VELOCITY = 0.5;
|
|
1262
1930
|
/**
|
|
1263
|
-
* Scrollable container
|
|
1264
|
-
*
|
|
1265
|
-
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
1266
|
-
* dynamic rendering optimization for off-screen items.
|
|
1931
|
+
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
1267
1932
|
*
|
|
1268
1933
|
* @example
|
|
1269
1934
|
* ```ts
|
|
@@ -1281,55 +1946,707 @@ const DIRECTION_MAP = {
|
|
|
1281
1946
|
* scene.container.addChild(scroll);
|
|
1282
1947
|
* ```
|
|
1283
1948
|
*/
|
|
1284
|
-
class ScrollContainer extends
|
|
1949
|
+
class ScrollContainer extends Container {
|
|
1950
|
+
__uiComponent = true;
|
|
1951
|
+
_viewport;
|
|
1952
|
+
_internalSetup = true;
|
|
1953
|
+
_content;
|
|
1954
|
+
_maskGfx;
|
|
1955
|
+
_bg = null;
|
|
1285
1956
|
_scrollConfig;
|
|
1957
|
+
_items = [];
|
|
1958
|
+
// Scrollbar
|
|
1959
|
+
_scrollbar = null;
|
|
1960
|
+
_scrollbarConfig;
|
|
1961
|
+
// Drag state
|
|
1962
|
+
_dragging = false;
|
|
1963
|
+
_dragStart = { x: 0, y: 0 };
|
|
1964
|
+
_contentStart = { x: 0, y: 0 };
|
|
1965
|
+
_velocity = { x: 0, y: 0 };
|
|
1966
|
+
_lastDragPos = { x: 0, y: 0 };
|
|
1967
|
+
_lastDragTime = 0;
|
|
1968
|
+
_inertiaActive = false;
|
|
1969
|
+
// Bound handlers for cleanup
|
|
1970
|
+
_onTickBound = null;
|
|
1971
|
+
_onWheelBound = null;
|
|
1286
1972
|
constructor(config) {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
radius: config.borderRadius ?? 0,
|
|
1973
|
+
super();
|
|
1974
|
+
this._viewport = { width: config.width, height: config.height };
|
|
1975
|
+
this._scrollConfig = {
|
|
1976
|
+
direction: config.direction ?? 'vertical',
|
|
1292
1977
|
elementsMargin: config.elementsMargin ?? 0,
|
|
1293
1978
|
padding: config.padding ?? 0,
|
|
1294
|
-
|
|
1979
|
+
borderRadius: config.borderRadius ?? 0,
|
|
1295
1980
|
disableEasing: config.disableEasing ?? false,
|
|
1296
|
-
globalScroll: config.globalScroll ?? true,
|
|
1297
1981
|
};
|
|
1982
|
+
// Background
|
|
1298
1983
|
if (config.backgroundColor !== undefined) {
|
|
1299
|
-
|
|
1984
|
+
this._bg = new Graphics();
|
|
1985
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
1986
|
+
.fill(config.backgroundColor);
|
|
1987
|
+
this.addChild(this._bg);
|
|
1988
|
+
}
|
|
1989
|
+
// Mask
|
|
1990
|
+
this._maskGfx = new Graphics();
|
|
1991
|
+
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
1992
|
+
.fill(0xffffff);
|
|
1993
|
+
this.addChild(this._maskGfx);
|
|
1994
|
+
// Content container
|
|
1995
|
+
this._content = new Container();
|
|
1996
|
+
this._content.mask = this._maskGfx;
|
|
1997
|
+
this.addChild(this._content);
|
|
1998
|
+
// Interaction
|
|
1999
|
+
this.eventMode = 'static';
|
|
2000
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
2001
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
2002
|
+
this.on('pointermove', this._onPointerMove, this);
|
|
2003
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
2004
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
2005
|
+
// Mouse wheel
|
|
2006
|
+
this._onWheelBound = this._onWheel.bind(this);
|
|
2007
|
+
// Scrollbar
|
|
2008
|
+
const sbWidth = config.scrollbarWidth ?? 6;
|
|
2009
|
+
const sbPadding = config.scrollbarPadding ?? 4;
|
|
2010
|
+
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
2011
|
+
if (config.scrollbar) {
|
|
2012
|
+
const customThumb = resolveView(config.thumbView);
|
|
2013
|
+
if (customThumb) {
|
|
2014
|
+
this._scrollbar = customThumb;
|
|
2015
|
+
}
|
|
2016
|
+
else {
|
|
2017
|
+
const g = new Graphics();
|
|
2018
|
+
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
2019
|
+
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
2020
|
+
this._scrollbar = g;
|
|
2021
|
+
}
|
|
2022
|
+
this._scrollbar.visible = false;
|
|
2023
|
+
super.addChild(this._scrollbar);
|
|
1300
2024
|
}
|
|
1301
|
-
|
|
1302
|
-
this._scrollConfig = config;
|
|
2025
|
+
this._internalSetup = false;
|
|
1303
2026
|
}
|
|
1304
|
-
/**
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
2027
|
+
/**
|
|
2028
|
+
* Override addChild so external children are routed to scroll content.
|
|
2029
|
+
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
2030
|
+
*/
|
|
2031
|
+
addChild(...children) {
|
|
2032
|
+
if (this._internalSetup) {
|
|
2033
|
+
return super.addChild(...children);
|
|
2034
|
+
}
|
|
2035
|
+
for (const child of children) {
|
|
2036
|
+
this.addItem(child);
|
|
2037
|
+
}
|
|
2038
|
+
return children[0];
|
|
2039
|
+
}
|
|
2040
|
+
removeChild(...children) {
|
|
2041
|
+
if (this._internalSetup) {
|
|
2042
|
+
return super.removeChild(...children);
|
|
2043
|
+
}
|
|
2044
|
+
for (const child of children) {
|
|
2045
|
+
const idx = this._items.indexOf(child);
|
|
2046
|
+
if (idx !== -1) {
|
|
2047
|
+
this._items.splice(idx, 1);
|
|
2048
|
+
this._content.removeChild(child);
|
|
1311
2049
|
}
|
|
1312
2050
|
}
|
|
1313
|
-
|
|
2051
|
+
this.layoutItems();
|
|
2052
|
+
return children[0];
|
|
2053
|
+
}
|
|
2054
|
+
/** React reconciler update hook */
|
|
2055
|
+
updateConfig(changed) {
|
|
2056
|
+
if ('width' in changed || 'height' in changed) {
|
|
2057
|
+
this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
2061
|
+
enableWheel(canvas) {
|
|
2062
|
+
if (this._onWheelBound) {
|
|
2063
|
+
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/** Set scrollable content. Replaces any existing items. */
|
|
2067
|
+
setContent(content) {
|
|
2068
|
+
this.clearItems();
|
|
1314
2069
|
const children = [...content.children];
|
|
1315
|
-
|
|
1316
|
-
this.
|
|
2070
|
+
for (const child of children) {
|
|
2071
|
+
this.addItem(child);
|
|
1317
2072
|
}
|
|
1318
2073
|
}
|
|
1319
2074
|
/** Add a single item */
|
|
1320
|
-
addItem(
|
|
1321
|
-
this.
|
|
1322
|
-
|
|
2075
|
+
addItem(child) {
|
|
2076
|
+
this._items.push(child);
|
|
2077
|
+
this._content.addChild(child);
|
|
2078
|
+
this.layoutItems();
|
|
2079
|
+
return this;
|
|
2080
|
+
}
|
|
2081
|
+
/** Remove all items */
|
|
2082
|
+
clearItems() {
|
|
2083
|
+
for (const item of this._items) {
|
|
2084
|
+
this._content.removeChild(item);
|
|
2085
|
+
}
|
|
2086
|
+
this._items.length = 0;
|
|
1323
2087
|
}
|
|
1324
|
-
/**
|
|
2088
|
+
/** Get items */
|
|
2089
|
+
get items() {
|
|
2090
|
+
return this._items;
|
|
2091
|
+
}
|
|
2092
|
+
/** Scroll to make a specific item index visible */
|
|
1325
2093
|
scrollToItem(index) {
|
|
1326
|
-
this.
|
|
2094
|
+
if (index < 0 || index >= this._items.length)
|
|
2095
|
+
return;
|
|
2096
|
+
const item = this._items[index];
|
|
2097
|
+
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
2098
|
+
if (isVert) {
|
|
2099
|
+
this._content.y = -item.y + this._scrollConfig.padding;
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
this._content.x = -item.x + this._scrollConfig.padding;
|
|
2103
|
+
}
|
|
2104
|
+
this.clampScroll();
|
|
1327
2105
|
}
|
|
1328
2106
|
/** Current scroll position */
|
|
1329
2107
|
get scrollPosition() {
|
|
1330
|
-
return { x: this.
|
|
2108
|
+
return { x: this._content.x, y: this._content.y };
|
|
2109
|
+
}
|
|
2110
|
+
/** Resize the scroll viewport */
|
|
2111
|
+
setViewportSize(width, height) {
|
|
2112
|
+
this._viewport.width = width;
|
|
2113
|
+
this._viewport.height = height;
|
|
2114
|
+
this._maskGfx.clear();
|
|
2115
|
+
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
2116
|
+
if (this._bg) {
|
|
2117
|
+
this._bg.clear();
|
|
2118
|
+
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
2119
|
+
.fill(0xffffff); // color will be overridden if needed
|
|
2120
|
+
}
|
|
2121
|
+
this.clampScroll();
|
|
2122
|
+
}
|
|
2123
|
+
// ─── Layout ──────────────────────────────────────────
|
|
2124
|
+
layoutItems() {
|
|
2125
|
+
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
2126
|
+
const isVert = direction !== 'horizontal';
|
|
2127
|
+
let pos = padding;
|
|
2128
|
+
for (const item of this._items) {
|
|
2129
|
+
if (isVert) {
|
|
2130
|
+
item.x = padding;
|
|
2131
|
+
item.y = pos;
|
|
2132
|
+
pos += item.height + elementsMargin;
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
item.x = pos;
|
|
2136
|
+
item.y = padding;
|
|
2137
|
+
pos += item.width + elementsMargin;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
// ─── Drag handling ───────────────────────────────────
|
|
2142
|
+
_onPointerDown(e) {
|
|
2143
|
+
this._dragging = true;
|
|
2144
|
+
this._inertiaActive = false;
|
|
2145
|
+
this._dragStart.x = e.globalX;
|
|
2146
|
+
this._dragStart.y = e.globalY;
|
|
2147
|
+
this._contentStart.x = this._content.x;
|
|
2148
|
+
this._contentStart.y = this._content.y;
|
|
2149
|
+
this._lastDragPos.x = e.globalX;
|
|
2150
|
+
this._lastDragPos.y = e.globalY;
|
|
2151
|
+
this._lastDragTime = Date.now();
|
|
2152
|
+
this._velocity.x = 0;
|
|
2153
|
+
this._velocity.y = 0;
|
|
2154
|
+
this.stopInertia();
|
|
2155
|
+
}
|
|
2156
|
+
_onPointerMove(e) {
|
|
2157
|
+
if (!this._dragging)
|
|
2158
|
+
return;
|
|
2159
|
+
const dx = e.globalX - this._dragStart.x;
|
|
2160
|
+
const dy = e.globalY - this._dragStart.y;
|
|
2161
|
+
const { direction } = this._scrollConfig;
|
|
2162
|
+
if (direction !== 'horizontal') {
|
|
2163
|
+
this._content.y = this._contentStart.y + dy;
|
|
2164
|
+
}
|
|
2165
|
+
if (direction !== 'vertical') {
|
|
2166
|
+
this._content.x = this._contentStart.x + dx;
|
|
2167
|
+
}
|
|
2168
|
+
// Track velocity
|
|
2169
|
+
const now = Date.now();
|
|
2170
|
+
const dt = now - this._lastDragTime;
|
|
2171
|
+
if (dt > 0) {
|
|
2172
|
+
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
2173
|
+
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
2174
|
+
}
|
|
2175
|
+
this._lastDragPos.x = e.globalX;
|
|
2176
|
+
this._lastDragPos.y = e.globalY;
|
|
2177
|
+
this._lastDragTime = now;
|
|
2178
|
+
this.clampScroll();
|
|
2179
|
+
}
|
|
2180
|
+
_onPointerUp() {
|
|
2181
|
+
if (!this._dragging)
|
|
2182
|
+
return;
|
|
2183
|
+
this._dragging = false;
|
|
2184
|
+
if (!this._scrollConfig.disableEasing &&
|
|
2185
|
+
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
2186
|
+
this.startInertia();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
// ─── Inertia ─────────────────────────────────────────
|
|
2190
|
+
startInertia() {
|
|
2191
|
+
this._inertiaActive = true;
|
|
2192
|
+
this._onTickBound = this._inertiaTick.bind(this);
|
|
2193
|
+
Ticker.shared.add(this._onTickBound);
|
|
2194
|
+
}
|
|
2195
|
+
stopInertia() {
|
|
2196
|
+
if (this._onTickBound && this._inertiaActive) {
|
|
2197
|
+
Ticker.shared.remove(this._onTickBound);
|
|
2198
|
+
this._inertiaActive = false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
_inertiaTick() {
|
|
2202
|
+
const { direction } = this._scrollConfig;
|
|
2203
|
+
if (direction !== 'horizontal') {
|
|
2204
|
+
this._content.y += this._velocity.y;
|
|
2205
|
+
this._velocity.y *= DECELERATION;
|
|
2206
|
+
}
|
|
2207
|
+
if (direction !== 'vertical') {
|
|
2208
|
+
this._content.x += this._velocity.x;
|
|
2209
|
+
this._velocity.x *= DECELERATION;
|
|
2210
|
+
}
|
|
2211
|
+
this.clampScroll();
|
|
2212
|
+
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
2213
|
+
this.stopInertia();
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
// ─── Mouse wheel ─────────────────────────────────────
|
|
2217
|
+
_onWheel(e) {
|
|
2218
|
+
const { direction } = this._scrollConfig;
|
|
2219
|
+
e.preventDefault();
|
|
2220
|
+
if (direction !== 'horizontal') {
|
|
2221
|
+
this._content.y -= e.deltaY;
|
|
2222
|
+
}
|
|
2223
|
+
if (direction !== 'vertical') {
|
|
2224
|
+
this._content.x -= e.deltaX;
|
|
2225
|
+
}
|
|
2226
|
+
this.clampScroll();
|
|
2227
|
+
}
|
|
2228
|
+
// ─── Scroll bounds ───────────────────────────────────
|
|
2229
|
+
clampScroll() {
|
|
2230
|
+
const { direction } = this._scrollConfig;
|
|
2231
|
+
const bounds = this._content.getLocalBounds();
|
|
2232
|
+
if (direction !== 'horizontal') {
|
|
2233
|
+
const contentHeight = bounds.height + bounds.y;
|
|
2234
|
+
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
2235
|
+
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
2236
|
+
}
|
|
2237
|
+
if (direction !== 'vertical') {
|
|
2238
|
+
const contentWidth = bounds.width + bounds.x;
|
|
2239
|
+
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
2240
|
+
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
2241
|
+
}
|
|
2242
|
+
this.updateScrollbar();
|
|
2243
|
+
}
|
|
2244
|
+
updateScrollbar() {
|
|
2245
|
+
if (!this._scrollbar)
|
|
2246
|
+
return;
|
|
2247
|
+
const { direction } = this._scrollConfig;
|
|
2248
|
+
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
2249
|
+
const bounds = this._content.getLocalBounds();
|
|
2250
|
+
const isVert = direction !== 'horizontal';
|
|
2251
|
+
if (isVert) {
|
|
2252
|
+
const contentH = bounds.height + bounds.y;
|
|
2253
|
+
if (contentH <= this._viewport.height) {
|
|
2254
|
+
this._scrollbar.visible = false;
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
this._scrollbar.visible = true;
|
|
2258
|
+
const ratio = this._viewport.height / contentH;
|
|
2259
|
+
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
2260
|
+
const scrollRange = this._viewport.height - thumbH;
|
|
2261
|
+
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
2262
|
+
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
2263
|
+
this._scrollbar.y = scrollProgress * scrollRange;
|
|
2264
|
+
this._scrollbar.height = thumbH;
|
|
2265
|
+
this._scrollbar.width = sbW;
|
|
2266
|
+
}
|
|
2267
|
+
else {
|
|
2268
|
+
const contentW = bounds.width + bounds.x;
|
|
2269
|
+
if (contentW <= this._viewport.width) {
|
|
2270
|
+
this._scrollbar.visible = false;
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
this._scrollbar.visible = true;
|
|
2274
|
+
const ratio = this._viewport.width / contentW;
|
|
2275
|
+
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
2276
|
+
const scrollRange = this._viewport.width - thumbW;
|
|
2277
|
+
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
2278
|
+
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
2279
|
+
this._scrollbar.x = scrollProgress * scrollRange;
|
|
2280
|
+
this._scrollbar.width = thumbW;
|
|
2281
|
+
this._scrollbar.height = sbW;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
destroy(options) {
|
|
2285
|
+
this.stopInertia();
|
|
2286
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
2287
|
+
this.off('pointermove', this._onPointerMove, this);
|
|
2288
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
2289
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
2290
|
+
this._items.length = 0;
|
|
2291
|
+
super.destroy(options);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* Draggable slider with customizable track, fill, and handle views.
|
|
2297
|
+
*
|
|
2298
|
+
* @example
|
|
2299
|
+
* ```ts
|
|
2300
|
+
* const volume = new Slider({
|
|
2301
|
+
* min: 0, max: 1, value: 0.5,
|
|
2302
|
+
* width: 200, height: 8,
|
|
2303
|
+
* fillColor: 0xffd700,
|
|
2304
|
+
* onUpdate: (v) => console.log('Volume:', v),
|
|
2305
|
+
* });
|
|
2306
|
+
* ```
|
|
2307
|
+
*/
|
|
2308
|
+
class Slider extends Container {
|
|
2309
|
+
__uiComponent = true;
|
|
2310
|
+
_track;
|
|
2311
|
+
_fill;
|
|
2312
|
+
_fillMask;
|
|
2313
|
+
_handle;
|
|
2314
|
+
_config;
|
|
2315
|
+
_value;
|
|
2316
|
+
_dragging = false;
|
|
2317
|
+
onUpdate = null;
|
|
2318
|
+
onChange = null;
|
|
2319
|
+
constructor(config = {}) {
|
|
2320
|
+
super();
|
|
2321
|
+
this._config = {
|
|
2322
|
+
min: config.min ?? 0,
|
|
2323
|
+
max: config.max ?? 1,
|
|
2324
|
+
step: config.step ?? 0,
|
|
2325
|
+
width: config.width ?? 200,
|
|
2326
|
+
height: config.height ?? 8,
|
|
2327
|
+
borderRadius: config.borderRadius ?? 4,
|
|
2328
|
+
trackColor: config.trackColor ?? 0x333333,
|
|
2329
|
+
fillColor: config.fillColor ?? 0xffd700,
|
|
2330
|
+
handleRadius: config.handleRadius ?? 12,
|
|
2331
|
+
handleColor: config.handleColor ?? 0xffffff,
|
|
2332
|
+
};
|
|
2333
|
+
this._value = config.value ?? this._config.min;
|
|
2334
|
+
this.onUpdate = config.onUpdate ?? null;
|
|
2335
|
+
this.onChange = config.onChange ?? null;
|
|
2336
|
+
const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
|
|
2337
|
+
// Track
|
|
2338
|
+
const customTrack = resolveView(config.trackView);
|
|
2339
|
+
if (customTrack) {
|
|
2340
|
+
customTrack.width = width;
|
|
2341
|
+
customTrack.height = height;
|
|
2342
|
+
this._track = customTrack;
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
const g = new Graphics();
|
|
2346
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
2347
|
+
this._track = g;
|
|
2348
|
+
}
|
|
2349
|
+
this.addChild(this._track);
|
|
2350
|
+
// Fill
|
|
2351
|
+
const customFill = resolveView(config.fillView);
|
|
2352
|
+
if (customFill) {
|
|
2353
|
+
customFill.width = width;
|
|
2354
|
+
customFill.height = height;
|
|
2355
|
+
this._fill = customFill;
|
|
2356
|
+
}
|
|
2357
|
+
else {
|
|
2358
|
+
const g = new Graphics();
|
|
2359
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
|
|
2360
|
+
this._fill = g;
|
|
2361
|
+
}
|
|
2362
|
+
this.addChild(this._fill);
|
|
2363
|
+
// Fill mask
|
|
2364
|
+
this._fillMask = new Graphics();
|
|
2365
|
+
this.addChild(this._fillMask);
|
|
2366
|
+
this._fill.mask = this._fillMask;
|
|
2367
|
+
// Handle
|
|
2368
|
+
const customHandle = resolveView(config.handleView);
|
|
2369
|
+
if (customHandle) {
|
|
2370
|
+
this._handle = customHandle;
|
|
2371
|
+
}
|
|
2372
|
+
else {
|
|
2373
|
+
const g = new Graphics();
|
|
2374
|
+
g.circle(0, 0, handleRadius).fill(handleColor);
|
|
2375
|
+
this._handle = g;
|
|
2376
|
+
}
|
|
2377
|
+
this._handle.y = height / 2;
|
|
2378
|
+
this.addChild(this._handle);
|
|
2379
|
+
// Interaction
|
|
2380
|
+
this.eventMode = 'static';
|
|
2381
|
+
this.cursor = 'pointer';
|
|
2382
|
+
// Hit area covers track + handle overflow
|
|
2383
|
+
const hitPad = Math.max(handleRadius - height / 2, 0);
|
|
2384
|
+
this.hitArea = { contains: (x, y) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
|
|
2385
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
2386
|
+
this.on('globalpointermove', this._onPointerMove, this);
|
|
2387
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
2388
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
2389
|
+
this._updateVisuals();
|
|
2390
|
+
}
|
|
2391
|
+
/** Current value */
|
|
2392
|
+
get value() {
|
|
2393
|
+
return this._value;
|
|
2394
|
+
}
|
|
2395
|
+
set value(v) {
|
|
2396
|
+
const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
|
|
2397
|
+
if (clamped === this._value)
|
|
2398
|
+
return;
|
|
2399
|
+
this._value = clamped;
|
|
2400
|
+
this._updateVisuals();
|
|
2401
|
+
}
|
|
2402
|
+
get min() { return this._config.min; }
|
|
2403
|
+
get max() { return this._config.max; }
|
|
2404
|
+
/** React reconciler update hook */
|
|
2405
|
+
updateConfig(changed) {
|
|
2406
|
+
if ('value' in changed)
|
|
2407
|
+
this.value = changed.value;
|
|
2408
|
+
if ('min' in changed) {
|
|
2409
|
+
this._config.min = changed.min;
|
|
2410
|
+
this._updateVisuals();
|
|
2411
|
+
}
|
|
2412
|
+
if ('max' in changed) {
|
|
2413
|
+
this._config.max = changed.max;
|
|
2414
|
+
this._updateVisuals();
|
|
2415
|
+
}
|
|
2416
|
+
if ('step' in changed)
|
|
2417
|
+
this._config.step = changed.step;
|
|
2418
|
+
if ('onUpdate' in changed)
|
|
2419
|
+
this.onUpdate = changed.onUpdate;
|
|
2420
|
+
if ('onChange' in changed)
|
|
2421
|
+
this.onChange = changed.onChange;
|
|
2422
|
+
}
|
|
2423
|
+
_fraction() {
|
|
2424
|
+
const { min, max } = this._config;
|
|
2425
|
+
return max === min ? 0 : (this._value - min) / (max - min);
|
|
2426
|
+
}
|
|
2427
|
+
_applyStep(v) {
|
|
2428
|
+
const { step, min } = this._config;
|
|
2429
|
+
if (step <= 0)
|
|
2430
|
+
return v;
|
|
2431
|
+
return min + Math.round((v - min) / step) * step;
|
|
2432
|
+
}
|
|
2433
|
+
_updateVisuals() {
|
|
2434
|
+
const frac = this._fraction();
|
|
2435
|
+
const w = this._config.width;
|
|
2436
|
+
const h = this._config.height;
|
|
2437
|
+
// Update fill mask
|
|
2438
|
+
this._fillMask.clear();
|
|
2439
|
+
this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
|
|
2440
|
+
// Update handle position
|
|
2441
|
+
this._handle.x = w * frac;
|
|
2442
|
+
}
|
|
2443
|
+
_valueFromPointer(e) {
|
|
2444
|
+
const local = this.toLocal(e.global);
|
|
2445
|
+
const frac = Math.max(0, Math.min(1, local.x / this._config.width));
|
|
2446
|
+
const { min, max } = this._config;
|
|
2447
|
+
return this._applyStep(min + frac * (max - min));
|
|
2448
|
+
}
|
|
2449
|
+
_onPointerDown(e) {
|
|
2450
|
+
this._dragging = true;
|
|
2451
|
+
const newValue = this._valueFromPointer(e);
|
|
2452
|
+
if (newValue !== this._value) {
|
|
2453
|
+
this._value = newValue;
|
|
2454
|
+
this._updateVisuals();
|
|
2455
|
+
this.onUpdate?.(this._value);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
_onPointerMove(e) {
|
|
2459
|
+
if (!this._dragging)
|
|
2460
|
+
return;
|
|
2461
|
+
const newValue = this._valueFromPointer(e);
|
|
2462
|
+
if (newValue !== this._value) {
|
|
2463
|
+
this._value = newValue;
|
|
2464
|
+
this._updateVisuals();
|
|
2465
|
+
this.onUpdate?.(this._value);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
_onPointerUp(_e) {
|
|
2469
|
+
if (!this._dragging)
|
|
2470
|
+
return;
|
|
2471
|
+
this._dragging = false;
|
|
2472
|
+
this.onChange?.(this._value);
|
|
2473
|
+
}
|
|
2474
|
+
destroy(options) {
|
|
2475
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
2476
|
+
this.off('globalpointermove', this._onPointerMove, this);
|
|
2477
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
2478
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
2479
|
+
this.onUpdate = null;
|
|
2480
|
+
this.onChange = null;
|
|
2481
|
+
super.destroy(options);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* Toggle switch with two states.
|
|
2487
|
+
*
|
|
2488
|
+
* Supports custom ON/OFF views or auto-generated Graphics-based toggle.
|
|
2489
|
+
* Click to toggle, or use `forceSwitch(value)` programmatically.
|
|
2490
|
+
*
|
|
2491
|
+
* @example
|
|
2492
|
+
* ```ts
|
|
2493
|
+
* const mute = new Toggle({
|
|
2494
|
+
* value: false,
|
|
2495
|
+
* onColor: 0x22cc22,
|
|
2496
|
+
* onChange: (on) => audioManager.mute(!on),
|
|
2497
|
+
* });
|
|
2498
|
+
* ```
|
|
2499
|
+
*/
|
|
2500
|
+
class Toggle extends Container {
|
|
2501
|
+
__uiComponent = true;
|
|
2502
|
+
_value;
|
|
2503
|
+
_onView = null;
|
|
2504
|
+
_offView = null;
|
|
2505
|
+
_handle = null;
|
|
2506
|
+
_trackGfx = null;
|
|
2507
|
+
_config;
|
|
2508
|
+
_useCustomViews;
|
|
2509
|
+
onChange = null;
|
|
2510
|
+
constructor(config = {}) {
|
|
2511
|
+
super();
|
|
2512
|
+
this._config = {
|
|
2513
|
+
width: config.width ?? 52,
|
|
2514
|
+
height: config.height ?? 28,
|
|
2515
|
+
onColor: config.onColor ?? 0x22cc22,
|
|
2516
|
+
offColor: config.offColor ?? 0x666666,
|
|
2517
|
+
handleColor: config.handleColor ?? 0xffffff,
|
|
2518
|
+
handleRadius: config.handleRadius ?? 0, // 0 = auto
|
|
2519
|
+
animationDuration: config.animationDuration ?? 200,
|
|
2520
|
+
};
|
|
2521
|
+
this._value = config.value ?? false;
|
|
2522
|
+
this.onChange = config.onChange ?? null;
|
|
2523
|
+
const customOn = resolveView(config.onView);
|
|
2524
|
+
const customOff = resolveView(config.offView);
|
|
2525
|
+
this._useCustomViews = !!(customOn || customOff);
|
|
2526
|
+
if (this._useCustomViews) {
|
|
2527
|
+
// Custom view mode: show/hide ON and OFF views
|
|
2528
|
+
if (customOn) {
|
|
2529
|
+
this._onView = customOn;
|
|
2530
|
+
this._onView.visible = this._value;
|
|
2531
|
+
this.addChild(this._onView);
|
|
2532
|
+
}
|
|
2533
|
+
if (customOff) {
|
|
2534
|
+
this._offView = customOff;
|
|
2535
|
+
this._offView.visible = !this._value;
|
|
2536
|
+
this.addChild(this._offView);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
else {
|
|
2540
|
+
// Graphics mode: track + sliding handle
|
|
2541
|
+
const { width, height, handleColor } = this._config;
|
|
2542
|
+
const handleRadius = this._config.handleRadius || (height / 2 - 3);
|
|
2543
|
+
this._config.handleRadius = handleRadius;
|
|
2544
|
+
this._trackGfx = new Graphics();
|
|
2545
|
+
this.addChild(this._trackGfx);
|
|
2546
|
+
this._drawTrack();
|
|
2547
|
+
const handle = new Graphics();
|
|
2548
|
+
handle.circle(0, 0, handleRadius).fill(handleColor);
|
|
2549
|
+
handle.y = height / 2;
|
|
2550
|
+
handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
|
|
2551
|
+
this._handle = handle;
|
|
2552
|
+
this.addChild(handle);
|
|
2553
|
+
}
|
|
2554
|
+
// Interaction
|
|
2555
|
+
this.eventMode = 'static';
|
|
2556
|
+
this.cursor = 'pointer';
|
|
2557
|
+
this.on('pointertap', this._onTap, this);
|
|
2558
|
+
}
|
|
2559
|
+
/** Current toggle state */
|
|
2560
|
+
get value() {
|
|
2561
|
+
return this._value;
|
|
2562
|
+
}
|
|
2563
|
+
set value(v) {
|
|
2564
|
+
if (v === this._value)
|
|
2565
|
+
return;
|
|
2566
|
+
this.forceSwitch(v);
|
|
2567
|
+
}
|
|
2568
|
+
/** Programmatically switch to a specific state with animation */
|
|
2569
|
+
forceSwitch(value) {
|
|
2570
|
+
this._value = value;
|
|
2571
|
+
this._animateToState();
|
|
2572
|
+
}
|
|
2573
|
+
/** React reconciler update hook */
|
|
2574
|
+
updateConfig(changed) {
|
|
2575
|
+
if ('value' in changed)
|
|
2576
|
+
this.value = changed.value;
|
|
2577
|
+
if ('onChange' in changed)
|
|
2578
|
+
this.onChange = changed.onChange;
|
|
2579
|
+
if ('animationDuration' in changed)
|
|
2580
|
+
this._config.animationDuration = changed.animationDuration;
|
|
2581
|
+
}
|
|
2582
|
+
_onTap() {
|
|
2583
|
+
this._value = !this._value;
|
|
2584
|
+
this._animateToState();
|
|
2585
|
+
this.onChange?.(this._value);
|
|
2586
|
+
}
|
|
2587
|
+
_animateToState() {
|
|
2588
|
+
const duration = this._config.animationDuration;
|
|
2589
|
+
if (this._useCustomViews) {
|
|
2590
|
+
// Custom views: crossfade
|
|
2591
|
+
if (this._onView) {
|
|
2592
|
+
Tween.killTweensOf(this._onView);
|
|
2593
|
+
if (this._value) {
|
|
2594
|
+
this._onView.visible = true;
|
|
2595
|
+
Tween.to(this._onView, { alpha: 1 }, duration);
|
|
2596
|
+
}
|
|
2597
|
+
else {
|
|
2598
|
+
Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
|
|
2599
|
+
if (this._onView)
|
|
2600
|
+
this._onView.visible = false;
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (this._offView) {
|
|
2605
|
+
Tween.killTweensOf(this._offView);
|
|
2606
|
+
if (!this._value) {
|
|
2607
|
+
this._offView.visible = true;
|
|
2608
|
+
Tween.to(this._offView, { alpha: 1 }, duration);
|
|
2609
|
+
}
|
|
2610
|
+
else {
|
|
2611
|
+
Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
|
|
2612
|
+
if (this._offView)
|
|
2613
|
+
this._offView.visible = false;
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
// Graphics mode: slide handle + recolor track
|
|
2620
|
+
this._drawTrack();
|
|
2621
|
+
if (this._handle) {
|
|
2622
|
+
const { width } = this._config;
|
|
2623
|
+
const handleRadius = this._config.handleRadius;
|
|
2624
|
+
const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
|
|
2625
|
+
Tween.killTweensOf(this._handle);
|
|
2626
|
+
Tween.to(this._handle, { x: targetX }, duration);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
_drawTrack() {
|
|
2631
|
+
if (!this._trackGfx)
|
|
2632
|
+
return;
|
|
2633
|
+
const { width, height, onColor, offColor } = this._config;
|
|
2634
|
+
const radius = height / 2;
|
|
2635
|
+
this._trackGfx.clear();
|
|
2636
|
+
this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
|
|
2637
|
+
}
|
|
2638
|
+
destroy(options) {
|
|
2639
|
+
this.off('pointertap', this._onTap, this);
|
|
2640
|
+
if (this._handle)
|
|
2641
|
+
Tween.killTweensOf(this._handle);
|
|
2642
|
+
if (this._onView)
|
|
2643
|
+
Tween.killTweensOf(this._onView);
|
|
2644
|
+
if (this._offView)
|
|
2645
|
+
Tween.killTweensOf(this._offView);
|
|
2646
|
+
this.onChange = null;
|
|
2647
|
+
super.destroy(options);
|
|
1331
2648
|
}
|
|
1332
2649
|
}
|
|
1333
2650
|
|
|
1334
|
-
export { BalanceDisplay, Button, Label, Layout, Modal, Panel, ProgressBar, ScrollContainer, Toast, WinDisplay };
|
|
2651
|
+
export { BalanceDisplay, Button, FlexContainer, Label, Layout, Modal, Panel, ProgressBar, ScrollContainer, Slider, Toast, Toggle, WinDisplay, resolveView };
|
|
1335
2652
|
//# sourceMappingURL=ui.esm.js.map
|