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