@energy8platform/game-engine 0.10.10 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -74
- package/dist/index.cjs.js +1280 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +362 -46
- package/dist/index.esm.js +1281 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +16 -21
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +16 -21
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2372 -11
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2372 -12
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1553 -632
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +374 -46
- package/dist/ui.esm.js +1553 -634
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +1 -11
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +1 -11
- package/dist/vite.esm.js.map +1 -1
- package/package.json +3 -18
- package/src/index.ts +3 -3
- package/src/lua/LuaEngine.ts +8 -18
- package/src/lua/SimulationRunner.ts +7 -3
- package/src/react/applyProps.ts +86 -0
- package/src/react/extendAll.ts +27 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +222 -0
- package/src/react/reconciler.ts +22 -5
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +479 -0
- package/src/ui/Label.ts +13 -0
- package/src/ui/Layout.ts +86 -87
- package/src/ui/Modal.ts +11 -1
- package/src/ui/Panel.ts +108 -36
- package/src/ui/ProgressBar.ts +85 -31
- package/src/ui/ScrollContainer.ts +397 -45
- package/src/ui/Toast.ts +47 -17
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +5 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
package/dist/index.esm.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Graphics, NineSliceSprite } from 'pixi.js';
|
|
1
|
+
import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Sprite, Graphics, NineSliceSprite } from 'pixi.js';
|
|
2
2
|
import { CasinoGameSDK, Bridge } from '@energy8platform/game-sdk';
|
|
3
3
|
export { BridgeDestroyedError, BridgeNotReadyError, SDKError, TimeoutError } from '@energy8platform/game-sdk';
|
|
4
|
-
import { FancyButton, ProgressBar as ProgressBar$1, ScrollBox } from '@pixi/ui';
|
|
5
|
-
import { LayoutContainer } from '@pixi/layout/components';
|
|
6
4
|
|
|
7
5
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
8
6
|
var ScaleMode;
|
|
@@ -2909,6 +2907,407 @@ class SpriteAnimation {
|
|
|
2909
2907
|
}
|
|
2910
2908
|
}
|
|
2911
2909
|
|
|
2910
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
2911
|
+
function normalizePadding(p) {
|
|
2912
|
+
return typeof p === 'number' ? [p, p, p, p] : p;
|
|
2913
|
+
}
|
|
2914
|
+
/** Measure a child's size and bounds offset for layout purposes */
|
|
2915
|
+
function measureChild(child) {
|
|
2916
|
+
const cfg = child._flexConfig;
|
|
2917
|
+
if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
|
|
2918
|
+
return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
|
|
2919
|
+
}
|
|
2920
|
+
// For FlexContainers, use their explicit size if set
|
|
2921
|
+
if (child instanceof FlexContainer) {
|
|
2922
|
+
const fc = child;
|
|
2923
|
+
if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
|
|
2924
|
+
return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
// Use localBounds to get the true visual extent and origin offset.
|
|
2928
|
+
// This handles children with non-zero anchors (e.g. Button, Label with centered text).
|
|
2929
|
+
const bounds = child.getLocalBounds();
|
|
2930
|
+
const w = cfg?.layoutWidth ?? bounds.width;
|
|
2931
|
+
const h = cfg?.layoutHeight ?? bounds.height;
|
|
2932
|
+
return { w, h, ox: bounds.x, oy: bounds.y };
|
|
2933
|
+
}
|
|
2934
|
+
function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
|
|
2935
|
+
if (items.length === 0)
|
|
2936
|
+
return;
|
|
2937
|
+
// Compute total fixed main size and flex grow total
|
|
2938
|
+
let totalFixed = 0;
|
|
2939
|
+
let totalGrow = 0;
|
|
2940
|
+
for (const item of items) {
|
|
2941
|
+
const grow = item.child._flexConfig?.flexGrow ?? 0;
|
|
2942
|
+
if (grow > 0) {
|
|
2943
|
+
totalGrow += grow;
|
|
2944
|
+
}
|
|
2945
|
+
else {
|
|
2946
|
+
totalFixed += isRow ? item.w : item.h;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
const totalGap = gap * (items.length - 1);
|
|
2950
|
+
const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
|
|
2951
|
+
// Resolve flex sizes
|
|
2952
|
+
if (totalGrow > 0) {
|
|
2953
|
+
for (const item of items) {
|
|
2954
|
+
const grow = item.child._flexConfig?.flexGrow ?? 0;
|
|
2955
|
+
if (grow > 0) {
|
|
2956
|
+
const flexSize = (grow / totalGrow) * availableForFlex;
|
|
2957
|
+
if (isRow) {
|
|
2958
|
+
item.w = flexSize;
|
|
2959
|
+
item.child.width = flexSize;
|
|
2960
|
+
}
|
|
2961
|
+
else {
|
|
2962
|
+
item.h = flexSize;
|
|
2963
|
+
item.child.height = flexSize;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
// Calculate total main size after flex
|
|
2969
|
+
let totalMain = totalGap;
|
|
2970
|
+
for (const item of items) {
|
|
2971
|
+
totalMain += isRow ? item.w : item.h;
|
|
2972
|
+
}
|
|
2973
|
+
// Justify: compute starting offset and extra spacing
|
|
2974
|
+
let mainOffset = 0;
|
|
2975
|
+
let extraGap = 0;
|
|
2976
|
+
switch (justify) {
|
|
2977
|
+
case 'start':
|
|
2978
|
+
break;
|
|
2979
|
+
case 'center':
|
|
2980
|
+
mainOffset = Math.max(0, (mainSize - totalMain) / 2);
|
|
2981
|
+
break;
|
|
2982
|
+
case 'end':
|
|
2983
|
+
mainOffset = Math.max(0, mainSize - totalMain);
|
|
2984
|
+
break;
|
|
2985
|
+
case 'space-between':
|
|
2986
|
+
if (items.length > 1) {
|
|
2987
|
+
extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
|
|
2988
|
+
}
|
|
2989
|
+
break;
|
|
2990
|
+
case 'space-around':
|
|
2991
|
+
if (items.length > 0) {
|
|
2992
|
+
const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
|
|
2993
|
+
const segment = totalSpace / items.length;
|
|
2994
|
+
mainOffset = segment / 2;
|
|
2995
|
+
extraGap = segment - gap;
|
|
2996
|
+
}
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
// Position each item
|
|
3000
|
+
let pos = mainOffset;
|
|
3001
|
+
for (const item of items) {
|
|
3002
|
+
const mainDim = isRow ? item.w : item.h;
|
|
3003
|
+
const crossDim = isRow ? item.h : item.w;
|
|
3004
|
+
// Cross-axis alignment
|
|
3005
|
+
let crossPos = crossOffset;
|
|
3006
|
+
switch (align) {
|
|
3007
|
+
case 'start':
|
|
3008
|
+
break;
|
|
3009
|
+
case 'center':
|
|
3010
|
+
crossPos += (crossSize - crossDim) / 2;
|
|
3011
|
+
break;
|
|
3012
|
+
case 'end':
|
|
3013
|
+
crossPos += crossSize - crossDim;
|
|
3014
|
+
break;
|
|
3015
|
+
case 'stretch':
|
|
3016
|
+
if (isRow) {
|
|
3017
|
+
item.child.height = crossSize;
|
|
3018
|
+
}
|
|
3019
|
+
else {
|
|
3020
|
+
item.child.width = crossSize;
|
|
3021
|
+
}
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
// Compensate for local bounds offset (e.g. centered anchors)
|
|
3025
|
+
if (isRow) {
|
|
3026
|
+
item.child.x = pos - item.ox;
|
|
3027
|
+
item.child.y = crossPos - item.oy;
|
|
3028
|
+
}
|
|
3029
|
+
else {
|
|
3030
|
+
item.child.x = crossPos - item.ox;
|
|
3031
|
+
item.child.y = pos - item.oy;
|
|
3032
|
+
}
|
|
3033
|
+
pos += mainDim + gap + extraGap;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
// ─── FlexContainer ───────────────────────────────────────
|
|
3037
|
+
/**
|
|
3038
|
+
* Lightweight flexbox-like layout container for PixiJS.
|
|
3039
|
+
*
|
|
3040
|
+
* Supports row/column direction, justify/align, gap, padding, wrapping,
|
|
3041
|
+
* and flex-grow distribution. Zero external dependencies.
|
|
3042
|
+
*
|
|
3043
|
+
* @example
|
|
3044
|
+
* ```ts
|
|
3045
|
+
* const toolbar = new FlexContainer({
|
|
3046
|
+
* direction: 'row',
|
|
3047
|
+
* justifyContent: 'space-between',
|
|
3048
|
+
* alignItems: 'center',
|
|
3049
|
+
* gap: 16,
|
|
3050
|
+
* padding: 12,
|
|
3051
|
+
* });
|
|
3052
|
+
*
|
|
3053
|
+
* toolbar.addFlexChild(button1);
|
|
3054
|
+
* toolbar.addFlexChild(button2);
|
|
3055
|
+
* toolbar.resize(800, 60);
|
|
3056
|
+
* ```
|
|
3057
|
+
*/
|
|
3058
|
+
class FlexContainer extends Container {
|
|
3059
|
+
__uiComponent = true;
|
|
3060
|
+
_config;
|
|
3061
|
+
_padding;
|
|
3062
|
+
_maxWidth;
|
|
3063
|
+
_maxHeight;
|
|
3064
|
+
/** @internal */ _explicitWidth;
|
|
3065
|
+
/** @internal */ _explicitHeight;
|
|
3066
|
+
_layoutChildren = [];
|
|
3067
|
+
_layoutDirty = true;
|
|
3068
|
+
constructor(config = {}) {
|
|
3069
|
+
super();
|
|
3070
|
+
this._config = {
|
|
3071
|
+
direction: config.direction ?? 'row',
|
|
3072
|
+
justifyContent: config.justifyContent ?? 'start',
|
|
3073
|
+
alignItems: config.alignItems ?? 'start',
|
|
3074
|
+
gap: config.gap ?? 0,
|
|
3075
|
+
flexWrap: config.flexWrap ?? false,
|
|
3076
|
+
};
|
|
3077
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
3078
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3079
|
+
this._maxHeight = config.maxHeight ?? Infinity;
|
|
3080
|
+
this._explicitWidth = config.width ?? 0;
|
|
3081
|
+
this._explicitHeight = config.height ?? 0;
|
|
3082
|
+
}
|
|
3083
|
+
// ─── Public API ──────────────────────────────────────
|
|
3084
|
+
/** Add a child with optional flex config. Also registers in flex layout. */
|
|
3085
|
+
addFlexChild(child, flexConfig) {
|
|
3086
|
+
if (flexConfig)
|
|
3087
|
+
child._flexConfig = flexConfig;
|
|
3088
|
+
if (!this._layoutChildren.includes(child)) {
|
|
3089
|
+
this._layoutChildren.push(child);
|
|
3090
|
+
this._layoutDirty = true;
|
|
3091
|
+
}
|
|
3092
|
+
super.addChild(child);
|
|
3093
|
+
return this;
|
|
3094
|
+
}
|
|
3095
|
+
/** Remove a child from flex layout and display list */
|
|
3096
|
+
removeFlexChild(child) {
|
|
3097
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
3098
|
+
if (idx !== -1) {
|
|
3099
|
+
this._layoutChildren.splice(idx, 1);
|
|
3100
|
+
this._layoutDirty = true;
|
|
3101
|
+
}
|
|
3102
|
+
super.removeChild(child);
|
|
3103
|
+
return this;
|
|
3104
|
+
}
|
|
3105
|
+
/** Remove all flex children */
|
|
3106
|
+
clearFlexChildren() {
|
|
3107
|
+
for (const child of this._layoutChildren) {
|
|
3108
|
+
super.removeChild(child);
|
|
3109
|
+
}
|
|
3110
|
+
this._layoutChildren.length = 0;
|
|
3111
|
+
this._layoutDirty = true;
|
|
3112
|
+
return this;
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Override addChild so children automatically participate in flex layout.
|
|
3116
|
+
* This enables declarative usage from React JSX.
|
|
3117
|
+
*/
|
|
3118
|
+
addChild(...children) {
|
|
3119
|
+
for (const child of children) {
|
|
3120
|
+
if (!this._layoutChildren.includes(child)) {
|
|
3121
|
+
this._layoutChildren.push(child);
|
|
3122
|
+
this._layoutDirty = true;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
const result = super.addChild(...children);
|
|
3126
|
+
if (this._layoutDirty)
|
|
3127
|
+
this.updateLayout();
|
|
3128
|
+
return result;
|
|
3129
|
+
}
|
|
3130
|
+
removeChild(...children) {
|
|
3131
|
+
for (const child of children) {
|
|
3132
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
3133
|
+
if (idx !== -1) {
|
|
3134
|
+
this._layoutChildren.splice(idx, 1);
|
|
3135
|
+
this._layoutDirty = true;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return super.removeChild(...children);
|
|
3139
|
+
}
|
|
3140
|
+
/** Get all flex layout children (read-only) */
|
|
3141
|
+
get flexChildren() {
|
|
3142
|
+
return this._layoutChildren;
|
|
3143
|
+
}
|
|
3144
|
+
/** Update the container size and recalculate layout */
|
|
3145
|
+
resize(width, height) {
|
|
3146
|
+
this._explicitWidth = width;
|
|
3147
|
+
this._explicitHeight = height;
|
|
3148
|
+
this._layoutDirty = true;
|
|
3149
|
+
this.updateLayout();
|
|
3150
|
+
}
|
|
3151
|
+
/** Update layout direction */
|
|
3152
|
+
setDirection(direction) {
|
|
3153
|
+
this._config.direction = direction;
|
|
3154
|
+
this._layoutDirty = true;
|
|
3155
|
+
}
|
|
3156
|
+
/** Update justifyContent */
|
|
3157
|
+
setJustifyContent(justify) {
|
|
3158
|
+
this._config.justifyContent = justify;
|
|
3159
|
+
this._layoutDirty = true;
|
|
3160
|
+
}
|
|
3161
|
+
/** Update alignItems */
|
|
3162
|
+
setAlignItems(align) {
|
|
3163
|
+
this._config.alignItems = align;
|
|
3164
|
+
this._layoutDirty = true;
|
|
3165
|
+
}
|
|
3166
|
+
/** Update gap */
|
|
3167
|
+
setGap(gap) {
|
|
3168
|
+
this._config.gap = gap;
|
|
3169
|
+
this._layoutDirty = true;
|
|
3170
|
+
}
|
|
3171
|
+
/** Update padding */
|
|
3172
|
+
setPadding(padding) {
|
|
3173
|
+
this._padding = normalizePadding(padding);
|
|
3174
|
+
this._layoutDirty = true;
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Recalculate and apply layout positions for all children.
|
|
3178
|
+
* Called automatically by `resize()`. Call manually after
|
|
3179
|
+
* adding/removing children without resize.
|
|
3180
|
+
*/
|
|
3181
|
+
updateLayout() {
|
|
3182
|
+
this._layoutDirty = false;
|
|
3183
|
+
const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
|
|
3184
|
+
const [pt, pr, pb, pl] = this._padding;
|
|
3185
|
+
const isRow = direction === 'row';
|
|
3186
|
+
const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
|
|
3187
|
+
const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
|
|
3188
|
+
const mainLimit = isRow ? contentW : contentH;
|
|
3189
|
+
const crossLimit = isRow ? contentH : contentW;
|
|
3190
|
+
// Measure children
|
|
3191
|
+
const measured = this._layoutChildren.map((child) => {
|
|
3192
|
+
const { w, h, ox, oy } = measureChild(child);
|
|
3193
|
+
return { child, w, h, ox, oy };
|
|
3194
|
+
});
|
|
3195
|
+
// Split into lines (if wrapping)
|
|
3196
|
+
const lines = [];
|
|
3197
|
+
if (flexWrap && mainLimit < Infinity) {
|
|
3198
|
+
let currentLine = [];
|
|
3199
|
+
let lineMain = 0;
|
|
3200
|
+
for (const item of measured) {
|
|
3201
|
+
const itemMain = isRow ? item.w : item.h;
|
|
3202
|
+
const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
|
|
3203
|
+
if (currentLine.length > 0 && wouldBe > mainLimit) {
|
|
3204
|
+
lines.push(currentLine);
|
|
3205
|
+
currentLine = [item];
|
|
3206
|
+
lineMain = itemMain;
|
|
3207
|
+
}
|
|
3208
|
+
else {
|
|
3209
|
+
currentLine.push(item);
|
|
3210
|
+
lineMain = wouldBe;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (currentLine.length > 0)
|
|
3214
|
+
lines.push(currentLine);
|
|
3215
|
+
}
|
|
3216
|
+
else {
|
|
3217
|
+
lines.push(measured);
|
|
3218
|
+
}
|
|
3219
|
+
// Compute cross size per line
|
|
3220
|
+
const lineCrossSizes = lines.map((line) => {
|
|
3221
|
+
let maxCross = 0;
|
|
3222
|
+
for (const item of line) {
|
|
3223
|
+
const cross = isRow ? item.h : item.w;
|
|
3224
|
+
if (cross > maxCross)
|
|
3225
|
+
maxCross = cross;
|
|
3226
|
+
}
|
|
3227
|
+
return maxCross;
|
|
3228
|
+
});
|
|
3229
|
+
// Layout each line
|
|
3230
|
+
let crossOffset = isRow ? pt : pl;
|
|
3231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3232
|
+
const line = lines[i];
|
|
3233
|
+
const lineCross = lineCrossSizes[i];
|
|
3234
|
+
const mainStart = isRow ? pl : pt;
|
|
3235
|
+
// Offset items by padding
|
|
3236
|
+
const tempItems = line.map((item) => ({ ...item }));
|
|
3237
|
+
layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
3238
|
+
// Apply main-axis padding offset
|
|
3239
|
+
for (const item of tempItems) {
|
|
3240
|
+
const origChild = line.find((l) => l.child === item.child);
|
|
3241
|
+
origChild.child.x = item.child.x + (isRow ? mainStart : 0);
|
|
3242
|
+
origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
|
|
3243
|
+
}
|
|
3244
|
+
crossOffset += lineCross + gap;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/** Computed content size (after layout) */
|
|
3248
|
+
getContentSize() {
|
|
3249
|
+
if (this._layoutDirty)
|
|
3250
|
+
this.updateLayout();
|
|
3251
|
+
let maxX = 0;
|
|
3252
|
+
let maxY = 0;
|
|
3253
|
+
for (const child of this._layoutChildren) {
|
|
3254
|
+
const { w, h } = measureChild(child);
|
|
3255
|
+
maxX = Math.max(maxX, child.x + w);
|
|
3256
|
+
maxY = Math.max(maxY, child.y + h);
|
|
3257
|
+
}
|
|
3258
|
+
const [, pr, pb] = this._padding;
|
|
3259
|
+
return { width: maxX + pr, height: maxY + pb };
|
|
3260
|
+
}
|
|
3261
|
+
/** React reconciler update hook — applies changed config props */
|
|
3262
|
+
updateConfig(changed) {
|
|
3263
|
+
if ('direction' in changed)
|
|
3264
|
+
this.setDirection(changed.direction);
|
|
3265
|
+
if ('justifyContent' in changed)
|
|
3266
|
+
this.setJustifyContent(changed.justifyContent);
|
|
3267
|
+
if ('alignItems' in changed)
|
|
3268
|
+
this.setAlignItems(changed.alignItems);
|
|
3269
|
+
if ('gap' in changed)
|
|
3270
|
+
this.setGap(changed.gap);
|
|
3271
|
+
if ('padding' in changed)
|
|
3272
|
+
this.setPadding(changed.padding);
|
|
3273
|
+
if ('flexWrap' in changed) {
|
|
3274
|
+
this._config.flexWrap = changed.flexWrap;
|
|
3275
|
+
this._layoutDirty = true;
|
|
3276
|
+
}
|
|
3277
|
+
if ('width' in changed || 'height' in changed) {
|
|
3278
|
+
this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
|
|
3279
|
+
return; // resize calls updateLayout
|
|
3280
|
+
}
|
|
3281
|
+
if (this._layoutDirty)
|
|
3282
|
+
this.updateLayout();
|
|
3283
|
+
}
|
|
3284
|
+
destroy(options) {
|
|
3285
|
+
this._layoutChildren.length = 0;
|
|
3286
|
+
super.destroy(options);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
/**
|
|
3291
|
+
* Resolve a ViewInput to a Container instance.
|
|
3292
|
+
*
|
|
3293
|
+
* @example
|
|
3294
|
+
* ```ts
|
|
3295
|
+
* resolveView('btn-idle') // → Sprite.from('btn-idle')
|
|
3296
|
+
* resolveView(someTexture) // → new Sprite(someTexture)
|
|
3297
|
+
* resolveView(myCustomContainer) // → myCustomContainer (as-is)
|
|
3298
|
+
* resolveView(undefined) // → null
|
|
3299
|
+
* ```
|
|
3300
|
+
*/
|
|
3301
|
+
function resolveView(input) {
|
|
3302
|
+
if (input == null)
|
|
3303
|
+
return null;
|
|
3304
|
+
if (typeof input === 'string')
|
|
3305
|
+
return Sprite.from(input);
|
|
3306
|
+
if (input instanceof Texture)
|
|
3307
|
+
return new Sprite(input);
|
|
3308
|
+
return input;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
2912
3311
|
const DEFAULT_COLORS = {
|
|
2913
3312
|
default: 0xffd700,
|
|
2914
3313
|
hover: 0xffe44d,
|
|
@@ -2917,33 +3316,55 @@ const DEFAULT_COLORS = {
|
|
|
2917
3316
|
};
|
|
2918
3317
|
function makeGraphicsView(w, h, radius, color) {
|
|
2919
3318
|
const g = new Graphics();
|
|
2920
|
-
g.roundRect(
|
|
2921
|
-
// Highlight overlay
|
|
2922
|
-
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
3319
|
+
g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
|
|
2923
3320
|
return g;
|
|
2924
3321
|
}
|
|
2925
3322
|
/**
|
|
2926
|
-
* Interactive button
|
|
3323
|
+
* Interactive button with per-state custom views and animations.
|
|
2927
3324
|
*
|
|
2928
|
-
*
|
|
2929
|
-
*
|
|
3325
|
+
* Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
|
|
3326
|
+
* (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
|
|
3327
|
+
* Falls back to colored Graphics when no custom view is provided.
|
|
2930
3328
|
*
|
|
2931
3329
|
* @example
|
|
2932
3330
|
* ```ts
|
|
3331
|
+
* // Graphics-based (quick prototyping)
|
|
2933
3332
|
* const btn = new Button({
|
|
2934
3333
|
* width: 200, height: 60, borderRadius: 12,
|
|
2935
3334
|
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2936
3335
|
* text: 'SPIN',
|
|
3336
|
+
* onPress: () => spin(),
|
|
3337
|
+
* });
|
|
3338
|
+
*
|
|
3339
|
+
* // Asset-based (production art)
|
|
3340
|
+
* const btn = new Button({
|
|
3341
|
+
* defaultView: 'btn-idle',
|
|
3342
|
+
* hoverView: 'btn-hover',
|
|
3343
|
+
* pressedView: 'btn-pressed',
|
|
3344
|
+
* disabledView: 'btn-disabled',
|
|
3345
|
+
* text: 'SPIN',
|
|
3346
|
+
* onPress: () => spin(),
|
|
2937
3347
|
* });
|
|
2938
3348
|
*
|
|
2939
|
-
*
|
|
2940
|
-
*
|
|
3349
|
+
* // Custom Container view
|
|
3350
|
+
* const btn = new Button({
|
|
3351
|
+
* defaultView: myAnimatedSprite,
|
|
3352
|
+
* text: 'SPIN',
|
|
3353
|
+
* });
|
|
2941
3354
|
* ```
|
|
2942
3355
|
*/
|
|
2943
|
-
class Button extends
|
|
2944
|
-
|
|
3356
|
+
class Button extends Container {
|
|
3357
|
+
__uiComponent = true;
|
|
3358
|
+
_views = new Map();
|
|
3359
|
+
_state = 'default';
|
|
3360
|
+
_enabled = true;
|
|
3361
|
+
_config;
|
|
3362
|
+
_textObj = null;
|
|
3363
|
+
/** Press callback */
|
|
3364
|
+
onPress;
|
|
2945
3365
|
constructor(config = {}) {
|
|
2946
|
-
|
|
3366
|
+
super();
|
|
3367
|
+
this._config = {
|
|
2947
3368
|
width: config.width ?? 200,
|
|
2948
3369
|
height: config.height ?? 60,
|
|
2949
3370
|
borderRadius: config.borderRadius ?? 8,
|
|
@@ -2951,50 +3372,39 @@ class Button extends FancyButton {
|
|
|
2951
3372
|
animationDuration: config.animationDuration ?? 100,
|
|
2952
3373
|
...config,
|
|
2953
3374
|
};
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
// Build FancyButton options
|
|
2957
|
-
const options = {
|
|
2958
|
-
anchor: 0.5,
|
|
2959
|
-
animations: {
|
|
2960
|
-
hover: {
|
|
2961
|
-
props: { scale: { x: 1.03, y: 1.03 } },
|
|
2962
|
-
duration: resolvedConfig.animationDuration,
|
|
2963
|
-
},
|
|
2964
|
-
pressed: {
|
|
2965
|
-
props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
|
|
2966
|
-
duration: resolvedConfig.animationDuration,
|
|
2967
|
-
},
|
|
2968
|
-
},
|
|
2969
|
-
};
|
|
2970
|
-
// Texture-based views
|
|
2971
|
-
if (config.textures) {
|
|
2972
|
-
if (config.textures.default)
|
|
2973
|
-
options.defaultView = config.textures.default;
|
|
2974
|
-
if (config.textures.hover)
|
|
2975
|
-
options.hoverView = config.textures.hover;
|
|
2976
|
-
if (config.textures.pressed)
|
|
2977
|
-
options.pressedView = config.textures.pressed;
|
|
2978
|
-
if (config.textures.disabled)
|
|
2979
|
-
options.disabledView = config.textures.disabled;
|
|
2980
|
-
}
|
|
2981
|
-
else {
|
|
2982
|
-
// Graphics-based views
|
|
2983
|
-
options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
|
|
2984
|
-
options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
|
|
2985
|
-
options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
|
|
2986
|
-
options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
|
|
2987
|
-
}
|
|
3375
|
+
this.onPress = config.onPress;
|
|
3376
|
+
this._buildViews(config);
|
|
2988
3377
|
// Text
|
|
2989
3378
|
if (config.text) {
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3379
|
+
this._textObj = new Text({
|
|
3380
|
+
text: config.text,
|
|
3381
|
+
style: {
|
|
3382
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
3383
|
+
fontSize: 20,
|
|
3384
|
+
fill: 0xffffff,
|
|
3385
|
+
fontWeight: 'bold',
|
|
3386
|
+
...config.textStyle,
|
|
3387
|
+
},
|
|
3388
|
+
});
|
|
3389
|
+
this._textObj.anchor.set(0.5);
|
|
3390
|
+
this.addChild(this._textObj);
|
|
3391
|
+
}
|
|
3392
|
+
// Interaction
|
|
3393
|
+
this.eventMode = 'static';
|
|
3394
|
+
this.cursor = 'pointer';
|
|
3395
|
+
this.on('pointerover', this._onPointerOver, this);
|
|
3396
|
+
this.on('pointerout', this._onPointerOut, this);
|
|
3397
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
3398
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
3399
|
+
this.on('pointerupoutside', this._onPointerUpOutside, this);
|
|
2994
3400
|
if (config.disabled) {
|
|
2995
3401
|
this.enabled = false;
|
|
2996
3402
|
}
|
|
2997
3403
|
}
|
|
3404
|
+
/** Current button state */
|
|
3405
|
+
get state() {
|
|
3406
|
+
return this._state;
|
|
3407
|
+
}
|
|
2998
3408
|
/** Enable the button */
|
|
2999
3409
|
enable() {
|
|
3000
3410
|
this.enabled = true;
|
|
@@ -3003,29 +3413,161 @@ class Button extends FancyButton {
|
|
|
3003
3413
|
disable() {
|
|
3004
3414
|
this.enabled = false;
|
|
3005
3415
|
}
|
|
3416
|
+
/** Whether the button is enabled */
|
|
3417
|
+
get enabled() {
|
|
3418
|
+
return this._enabled;
|
|
3419
|
+
}
|
|
3420
|
+
set enabled(value) {
|
|
3421
|
+
this._enabled = value;
|
|
3422
|
+
this.cursor = value ? 'pointer' : 'default';
|
|
3423
|
+
this.eventMode = value ? 'static' : 'none';
|
|
3424
|
+
this._setState(value ? 'default' : 'disabled');
|
|
3425
|
+
}
|
|
3006
3426
|
/** Whether the button is disabled */
|
|
3007
3427
|
get disabled() {
|
|
3008
|
-
return !this.
|
|
3428
|
+
return !this._enabled;
|
|
3429
|
+
}
|
|
3430
|
+
/** Update button text */
|
|
3431
|
+
set text(value) {
|
|
3432
|
+
if (this._textObj) {
|
|
3433
|
+
this._textObj.text = value;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
// ─── View building ──────────────────────────────────
|
|
3437
|
+
_buildViews(config) {
|
|
3438
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
3439
|
+
const { width, height, borderRadius } = this._config;
|
|
3440
|
+
const stateViews = {
|
|
3441
|
+
default: config.defaultView,
|
|
3442
|
+
hover: config.hoverView,
|
|
3443
|
+
pressed: config.pressedView,
|
|
3444
|
+
disabled: config.disabledView,
|
|
3445
|
+
};
|
|
3446
|
+
const states = ['default', 'hover', 'pressed', 'disabled'];
|
|
3447
|
+
for (const state of states) {
|
|
3448
|
+
const customView = resolveView(stateViews[state]);
|
|
3449
|
+
const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
|
|
3450
|
+
view.visible = state === 'default';
|
|
3451
|
+
this._views.set(state, view);
|
|
3452
|
+
this.addChild(view);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
_rebuildViews() {
|
|
3456
|
+
for (const [, view] of this._views) {
|
|
3457
|
+
this.removeChild(view);
|
|
3458
|
+
view.destroy();
|
|
3459
|
+
}
|
|
3460
|
+
this._views.clear();
|
|
3461
|
+
this._buildViews(this._config);
|
|
3462
|
+
// Re-insert views before text
|
|
3463
|
+
if (this._textObj && this._textObj.parent === this) {
|
|
3464
|
+
this.setChildIndex(this._textObj, this.children.length - 1);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
// ─── State management ───────────────────────────────
|
|
3468
|
+
_setState(state) {
|
|
3469
|
+
if (this._state === state)
|
|
3470
|
+
return;
|
|
3471
|
+
this._state = state;
|
|
3472
|
+
for (const [s, view] of this._views) {
|
|
3473
|
+
view.visible = s === state;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
_onPointerOver() {
|
|
3477
|
+
if (!this._enabled)
|
|
3478
|
+
return;
|
|
3479
|
+
this._setState('hover');
|
|
3480
|
+
Tween.killTweensOf(this);
|
|
3481
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3482
|
+
}
|
|
3483
|
+
_onPointerOut() {
|
|
3484
|
+
if (!this._enabled)
|
|
3485
|
+
return;
|
|
3486
|
+
this._setState('default');
|
|
3487
|
+
Tween.killTweensOf(this);
|
|
3488
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3489
|
+
}
|
|
3490
|
+
_onPointerDown() {
|
|
3491
|
+
if (!this._enabled)
|
|
3492
|
+
return;
|
|
3493
|
+
this._setState('pressed');
|
|
3494
|
+
Tween.killTweensOf(this);
|
|
3495
|
+
const s = this._config.pressScale;
|
|
3496
|
+
Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3497
|
+
}
|
|
3498
|
+
_onPointerUp() {
|
|
3499
|
+
if (!this._enabled)
|
|
3500
|
+
return;
|
|
3501
|
+
this._setState('hover');
|
|
3502
|
+
Tween.killTweensOf(this);
|
|
3503
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3504
|
+
this.onPress?.();
|
|
3505
|
+
}
|
|
3506
|
+
_onPointerUpOutside() {
|
|
3507
|
+
if (!this._enabled)
|
|
3508
|
+
return;
|
|
3509
|
+
this._setState('default');
|
|
3510
|
+
Tween.killTweensOf(this);
|
|
3511
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3512
|
+
}
|
|
3513
|
+
/** React reconciler update hook */
|
|
3514
|
+
updateConfig(changed) {
|
|
3515
|
+
if ('text' in changed && this._textObj)
|
|
3516
|
+
this._textObj.text = changed.text;
|
|
3517
|
+
if ('disabled' in changed)
|
|
3518
|
+
this.enabled = !changed.disabled;
|
|
3519
|
+
if ('onPress' in changed)
|
|
3520
|
+
this.onPress = changed.onPress;
|
|
3521
|
+
const structural = [
|
|
3522
|
+
'colors', 'width', 'height', 'borderRadius', 'textStyle',
|
|
3523
|
+
'defaultView', 'hoverView', 'pressedView', 'disabledView',
|
|
3524
|
+
];
|
|
3525
|
+
const needsRebuild = structural.some((k) => k in changed);
|
|
3526
|
+
if (needsRebuild) {
|
|
3527
|
+
Object.assign(this._config, changed);
|
|
3528
|
+
this._rebuildViews();
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
destroy(options) {
|
|
3532
|
+
Tween.killTweensOf(this);
|
|
3533
|
+
this.off('pointerover', this._onPointerOver, this);
|
|
3534
|
+
this.off('pointerout', this._onPointerOut, this);
|
|
3535
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
3536
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
3537
|
+
this.off('pointerupoutside', this._onPointerUpOutside, this);
|
|
3538
|
+
this._views.clear();
|
|
3539
|
+
this._textObj = null;
|
|
3540
|
+
super.destroy(options);
|
|
3009
3541
|
}
|
|
3010
3542
|
}
|
|
3011
3543
|
|
|
3012
|
-
function makeBarGraphics(w, h, radius, color) {
|
|
3013
|
-
return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
3014
|
-
}
|
|
3015
3544
|
/**
|
|
3016
|
-
* Horizontal progress bar
|
|
3545
|
+
* Horizontal progress bar with optional custom track/fill views.
|
|
3017
3546
|
*
|
|
3018
|
-
*
|
|
3547
|
+
* Supports asset-based skinning: provide `trackView` and/or `fillView`
|
|
3548
|
+
* as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
|
|
3549
|
+
* Falls back to colored Graphics when no custom views are provided.
|
|
3019
3550
|
*
|
|
3020
3551
|
* @example
|
|
3021
3552
|
* ```ts
|
|
3553
|
+
* // Graphics-based (quick prototyping)
|
|
3022
3554
|
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3555
|
+
* bar.progress = 0.5;
|
|
3556
|
+
*
|
|
3557
|
+
* // Asset-based (production art)
|
|
3558
|
+
* const bar = new ProgressBar({
|
|
3559
|
+
* width: 300, height: 20,
|
|
3560
|
+
* trackView: 'bar-track',
|
|
3561
|
+
* fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
|
|
3562
|
+
* });
|
|
3563
|
+
* bar.progress = 0.75;
|
|
3025
3564
|
* ```
|
|
3026
3565
|
*/
|
|
3027
3566
|
class ProgressBar extends Container {
|
|
3028
|
-
|
|
3567
|
+
__uiComponent = true;
|
|
3568
|
+
_track;
|
|
3569
|
+
_fill;
|
|
3570
|
+
_fillMask;
|
|
3029
3571
|
_borderGfx;
|
|
3030
3572
|
_config;
|
|
3031
3573
|
_progress = 0;
|
|
@@ -3044,21 +3586,39 @@ class ProgressBar extends Container {
|
|
|
3044
3586
|
animationSpeed: config.animationSpeed ?? 0.1,
|
|
3045
3587
|
};
|
|
3046
3588
|
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
3047
|
-
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3589
|
+
// Track background — custom view or Graphics
|
|
3590
|
+
const customTrack = resolveView(config.trackView);
|
|
3591
|
+
if (customTrack) {
|
|
3592
|
+
customTrack.width = width;
|
|
3593
|
+
customTrack.height = height;
|
|
3594
|
+
this._track = customTrack;
|
|
3595
|
+
}
|
|
3596
|
+
else {
|
|
3597
|
+
const g = new Graphics();
|
|
3598
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
3599
|
+
this._track = g;
|
|
3600
|
+
}
|
|
3601
|
+
this.addChild(this._track);
|
|
3602
|
+
// Fill bar — custom view or Graphics
|
|
3603
|
+
const customFill = resolveView(config.fillView);
|
|
3604
|
+
if (customFill) {
|
|
3605
|
+
customFill.x = borderWidth;
|
|
3606
|
+
customFill.y = borderWidth;
|
|
3607
|
+
customFill.width = width - borderWidth * 2;
|
|
3608
|
+
customFill.height = height - borderWidth * 2;
|
|
3609
|
+
this._fill = customFill;
|
|
3610
|
+
}
|
|
3611
|
+
else {
|
|
3612
|
+
const g = new Graphics();
|
|
3613
|
+
g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
|
|
3614
|
+
this._fill = g;
|
|
3615
|
+
}
|
|
3616
|
+
this.addChild(this._fill);
|
|
3617
|
+
// Mask for the fill (controls visible width)
|
|
3618
|
+
this._fillMask = new Graphics();
|
|
3619
|
+
this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
|
|
3620
|
+
this.addChild(this._fillMask);
|
|
3621
|
+
this._fill.mask = this._fillMask;
|
|
3062
3622
|
// Border overlay
|
|
3063
3623
|
this._borderGfx = new Graphics();
|
|
3064
3624
|
if (borderColor !== undefined && borderWidth > 0) {
|
|
@@ -3076,7 +3636,7 @@ class ProgressBar extends Container {
|
|
|
3076
3636
|
this._progress = Math.max(0, Math.min(1, value));
|
|
3077
3637
|
if (!this._config.animated) {
|
|
3078
3638
|
this._displayedProgress = this._progress;
|
|
3079
|
-
this.
|
|
3639
|
+
this.updateMask();
|
|
3080
3640
|
}
|
|
3081
3641
|
}
|
|
3082
3642
|
/**
|
|
@@ -3087,11 +3647,26 @@ class ProgressBar extends Container {
|
|
|
3087
3647
|
return;
|
|
3088
3648
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
3089
3649
|
this._displayedProgress = this._progress;
|
|
3650
|
+
this.updateMask();
|
|
3090
3651
|
return;
|
|
3091
3652
|
}
|
|
3092
3653
|
this._displayedProgress +=
|
|
3093
3654
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
3094
|
-
this.
|
|
3655
|
+
this.updateMask();
|
|
3656
|
+
}
|
|
3657
|
+
/** React reconciler update hook */
|
|
3658
|
+
updateConfig(changed) {
|
|
3659
|
+
if ('progress' in changed)
|
|
3660
|
+
this.progress = changed.progress;
|
|
3661
|
+
if ('animated' in changed)
|
|
3662
|
+
this._config.animated = changed.animated;
|
|
3663
|
+
if ('animationSpeed' in changed)
|
|
3664
|
+
this._config.animationSpeed = changed.animationSpeed;
|
|
3665
|
+
}
|
|
3666
|
+
updateMask() {
|
|
3667
|
+
const w = this._config.width * this._displayedProgress;
|
|
3668
|
+
this._fillMask.clear();
|
|
3669
|
+
this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
|
|
3095
3670
|
}
|
|
3096
3671
|
}
|
|
3097
3672
|
|
|
@@ -3109,6 +3684,7 @@ class ProgressBar extends Container {
|
|
|
3109
3684
|
* ```
|
|
3110
3685
|
*/
|
|
3111
3686
|
class Label extends Container {
|
|
3687
|
+
__uiComponent = true;
|
|
3112
3688
|
_text;
|
|
3113
3689
|
_maxWidth;
|
|
3114
3690
|
_autoFit;
|
|
@@ -3175,6 +3751,21 @@ class Label extends Container {
|
|
|
3175
3751
|
maximumFractionDigits: decimals,
|
|
3176
3752
|
}).format(value);
|
|
3177
3753
|
}
|
|
3754
|
+
/** React reconciler update hook */
|
|
3755
|
+
updateConfig(changed) {
|
|
3756
|
+
if ('text' in changed)
|
|
3757
|
+
this.text = changed.text;
|
|
3758
|
+
if ('maxWidth' in changed)
|
|
3759
|
+
this.maxWidth = changed.maxWidth;
|
|
3760
|
+
if ('autoFit' in changed) {
|
|
3761
|
+
this._autoFit = changed.autoFit;
|
|
3762
|
+
this.fitText();
|
|
3763
|
+
}
|
|
3764
|
+
if ('style' in changed && typeof changed.style === 'object') {
|
|
3765
|
+
Object.assign(this._text.style, changed.style);
|
|
3766
|
+
this.fitText();
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3178
3769
|
fitText() {
|
|
3179
3770
|
if (!this._autoFit || this._maxWidth === Infinity)
|
|
3180
3771
|
return;
|
|
@@ -3187,10 +3778,10 @@ class Label extends Container {
|
|
|
3187
3778
|
}
|
|
3188
3779
|
|
|
3189
3780
|
/**
|
|
3190
|
-
* Background panel
|
|
3781
|
+
* Background panel with optional flexbox content layout.
|
|
3191
3782
|
*
|
|
3192
3783
|
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
3193
|
-
* Children added
|
|
3784
|
+
* Children added via `addContent()` participate in flex layout automatically.
|
|
3194
3785
|
*
|
|
3195
3786
|
* @example
|
|
3196
3787
|
* ```ts
|
|
@@ -3205,9 +3796,14 @@ class Label extends Container {
|
|
|
3205
3796
|
* });
|
|
3206
3797
|
* ```
|
|
3207
3798
|
*/
|
|
3208
|
-
class Panel extends
|
|
3799
|
+
class Panel extends Container {
|
|
3800
|
+
__uiComponent = true;
|
|
3801
|
+
_bg;
|
|
3802
|
+
_content;
|
|
3803
|
+
_internalSetup = true;
|
|
3209
3804
|
_panelConfig;
|
|
3210
3805
|
constructor(config = {}) {
|
|
3806
|
+
super();
|
|
3211
3807
|
const resolvedConfig = {
|
|
3212
3808
|
width: config.width ?? 400,
|
|
3213
3809
|
height: config.height ?? 300,
|
|
@@ -3215,8 +3811,8 @@ class Panel extends LayoutContainer {
|
|
|
3215
3811
|
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
3216
3812
|
...config,
|
|
3217
3813
|
};
|
|
3218
|
-
|
|
3219
|
-
|
|
3814
|
+
this._panelConfig = resolvedConfig;
|
|
3815
|
+
// Create background
|
|
3220
3816
|
if (config.nineSliceTexture) {
|
|
3221
3817
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
3222
3818
|
? Texture.from(config.nineSliceTexture)
|
|
@@ -3232,40 +3828,102 @@ class Panel extends LayoutContainer {
|
|
|
3232
3828
|
nineSlice.width = resolvedConfig.width;
|
|
3233
3829
|
nineSlice.height = resolvedConfig.height;
|
|
3234
3830
|
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
3235
|
-
|
|
3831
|
+
this._bg = nineSlice;
|
|
3236
3832
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
height: resolvedConfig.height,
|
|
3243
|
-
padding: resolvedConfig.padding,
|
|
3244
|
-
flexDirection: 'column',
|
|
3245
|
-
};
|
|
3246
|
-
// Graphics-based background via layout styles
|
|
3247
|
-
if (!config.nineSliceTexture) {
|
|
3248
|
-
layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3249
|
-
layoutStyles.borderRadius = config.borderRadius ?? 0;
|
|
3833
|
+
else {
|
|
3834
|
+
const g = new Graphics();
|
|
3835
|
+
const bgColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3836
|
+
const radius = config.borderRadius ?? 0;
|
|
3837
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
|
|
3250
3838
|
if (config.borderColor !== undefined && config.borderWidth) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3839
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
|
|
3840
|
+
.stroke({ color: config.borderColor, width: config.borderWidth });
|
|
3253
3841
|
}
|
|
3842
|
+
g.alpha = resolvedConfig.backgroundAlpha;
|
|
3843
|
+
this._bg = g;
|
|
3254
3844
|
}
|
|
3255
|
-
this.
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3845
|
+
this.addChild(this._bg);
|
|
3846
|
+
// Create content flex container
|
|
3847
|
+
this._content = new FlexContainer({
|
|
3848
|
+
...config.layout,
|
|
3849
|
+
direction: config.layout?.direction ?? 'column',
|
|
3850
|
+
justifyContent: config.layout?.justifyContent ?? 'start',
|
|
3851
|
+
alignItems: config.layout?.alignItems ?? 'start',
|
|
3852
|
+
gap: config.layout?.gap ?? 0,
|
|
3853
|
+
padding: resolvedConfig.padding,
|
|
3854
|
+
width: resolvedConfig.width,
|
|
3855
|
+
height: resolvedConfig.height,
|
|
3856
|
+
});
|
|
3857
|
+
this.addChild(this._content);
|
|
3858
|
+
this._internalSetup = false;
|
|
3259
3859
|
}
|
|
3260
|
-
/** Access the content container
|
|
3860
|
+
/** Access the content flex container — add children here for layout */
|
|
3261
3861
|
get content() {
|
|
3262
|
-
return this.
|
|
3862
|
+
return this._content;
|
|
3863
|
+
}
|
|
3864
|
+
/** Convenience: add a child to the content layout */
|
|
3865
|
+
addContent(child) {
|
|
3866
|
+
this._content.addFlexChild(child);
|
|
3867
|
+
this._content.updateLayout();
|
|
3868
|
+
return this;
|
|
3263
3869
|
}
|
|
3264
3870
|
/** Resize the panel */
|
|
3265
3871
|
setSize(width, height) {
|
|
3266
3872
|
this._panelConfig.width = width;
|
|
3267
3873
|
this._panelConfig.height = height;
|
|
3268
|
-
|
|
3874
|
+
// Resize background
|
|
3875
|
+
if (this._bg instanceof NineSliceSprite) {
|
|
3876
|
+
this._bg.width = width;
|
|
3877
|
+
this._bg.height = height;
|
|
3878
|
+
}
|
|
3879
|
+
else if (this._bg instanceof Graphics) {
|
|
3880
|
+
const radius = this._panelConfig.borderRadius ?? 0;
|
|
3881
|
+
const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
|
|
3882
|
+
this._bg.clear();
|
|
3883
|
+
this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
|
|
3884
|
+
if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
|
|
3885
|
+
this._bg.roundRect(0, 0, width, height, radius)
|
|
3886
|
+
.stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
|
|
3887
|
+
}
|
|
3888
|
+
this._bg.alpha = this._panelConfig.backgroundAlpha;
|
|
3889
|
+
}
|
|
3890
|
+
this._content.resize(width, height);
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Override addChild so external children are routed to content FlexContainer.
|
|
3894
|
+
* Enables `<panel><label /><button /></panel>` in React JSX.
|
|
3895
|
+
*/
|
|
3896
|
+
addChild(...children) {
|
|
3897
|
+
if (this._internalSetup) {
|
|
3898
|
+
return super.addChild(...children);
|
|
3899
|
+
}
|
|
3900
|
+
for (const child of children) {
|
|
3901
|
+
this._content.addFlexChild(child);
|
|
3902
|
+
}
|
|
3903
|
+
this._content.updateLayout();
|
|
3904
|
+
return children[0];
|
|
3905
|
+
}
|
|
3906
|
+
removeChild(...children) {
|
|
3907
|
+
if (this._internalSetup) {
|
|
3908
|
+
return super.removeChild(...children);
|
|
3909
|
+
}
|
|
3910
|
+
for (const child of children) {
|
|
3911
|
+
this._content.removeFlexChild(child);
|
|
3912
|
+
}
|
|
3913
|
+
return children[0];
|
|
3914
|
+
}
|
|
3915
|
+
/** React reconciler update hook */
|
|
3916
|
+
updateConfig(changed) {
|
|
3917
|
+
if ('width' in changed || 'height' in changed) {
|
|
3918
|
+
this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
|
|
3919
|
+
}
|
|
3920
|
+
if ('backgroundAlpha' in changed) {
|
|
3921
|
+
this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
|
|
3922
|
+
this._bg.alpha = changed.backgroundAlpha;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
destroy(options) {
|
|
3926
|
+
super.destroy(options);
|
|
3269
3927
|
}
|
|
3270
3928
|
}
|
|
3271
3929
|
|
|
@@ -3273,7 +3931,7 @@ class Panel extends LayoutContainer {
|
|
|
3273
3931
|
* Reactive balance display component.
|
|
3274
3932
|
*
|
|
3275
3933
|
* Automatically formats currency and can animate value changes
|
|
3276
|
-
* with a smooth countup/countdown effect.
|
|
3934
|
+
* with a smooth countup/countdown effect using engine Tween.
|
|
3277
3935
|
*
|
|
3278
3936
|
* @example
|
|
3279
3937
|
* ```ts
|
|
@@ -3285,13 +3943,14 @@ class Panel extends LayoutContainer {
|
|
|
3285
3943
|
* ```
|
|
3286
3944
|
*/
|
|
3287
3945
|
class BalanceDisplay extends Container {
|
|
3946
|
+
__uiComponent = true;
|
|
3288
3947
|
_prefixLabel = null;
|
|
3289
3948
|
_valueLabel;
|
|
3290
3949
|
_config;
|
|
3291
3950
|
_currentValue = 0;
|
|
3292
3951
|
_displayedValue = 0;
|
|
3293
|
-
|
|
3294
|
-
|
|
3952
|
+
/** Internal target for Tween animation */
|
|
3953
|
+
_tweenTarget = { value: 0 };
|
|
3295
3954
|
constructor(config = {}) {
|
|
3296
3955
|
super();
|
|
3297
3956
|
this._config = {
|
|
@@ -3352,37 +4011,13 @@ class BalanceDisplay extends Container {
|
|
|
3352
4011
|
this._config.currency = currency;
|
|
3353
4012
|
this.updateDisplay();
|
|
3354
4013
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
this.
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
const startTime = Date.now();
|
|
3363
|
-
return new Promise((resolve) => {
|
|
3364
|
-
const tick = () => {
|
|
3365
|
-
if (this._animationCancelled) {
|
|
3366
|
-
this._animating = false;
|
|
3367
|
-
resolve();
|
|
3368
|
-
return;
|
|
3369
|
-
}
|
|
3370
|
-
const elapsed = Date.now() - startTime;
|
|
3371
|
-
const t = Math.min(elapsed / duration, 1);
|
|
3372
|
-
const eased = Easing.easeOutCubic(t);
|
|
3373
|
-
this._displayedValue = from + (to - from) * eased;
|
|
3374
|
-
this.updateDisplay();
|
|
3375
|
-
if (t < 1) {
|
|
3376
|
-
requestAnimationFrame(tick);
|
|
3377
|
-
}
|
|
3378
|
-
else {
|
|
3379
|
-
this._displayedValue = to;
|
|
3380
|
-
this.updateDisplay();
|
|
3381
|
-
this._animating = false;
|
|
3382
|
-
resolve();
|
|
3383
|
-
}
|
|
3384
|
-
};
|
|
3385
|
-
requestAnimationFrame(tick);
|
|
4014
|
+
animateValue(from, to) {
|
|
4015
|
+
// Cancel any running animation
|
|
4016
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4017
|
+
this._tweenTarget.value = from;
|
|
4018
|
+
Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
|
|
4019
|
+
this._displayedValue = this._tweenTarget.value;
|
|
4020
|
+
this.updateDisplay();
|
|
3386
4021
|
});
|
|
3387
4022
|
}
|
|
3388
4023
|
updateDisplay() {
|
|
@@ -3394,13 +4029,24 @@ class BalanceDisplay extends Container {
|
|
|
3394
4029
|
this._valueLabel.y = 14;
|
|
3395
4030
|
}
|
|
3396
4031
|
}
|
|
4032
|
+
/** React reconciler update hook */
|
|
4033
|
+
updateConfig(changed) {
|
|
4034
|
+
if ('value' in changed)
|
|
4035
|
+
this.setValue(changed.value);
|
|
4036
|
+
if ('currency' in changed)
|
|
4037
|
+
this.setCurrency(changed.currency);
|
|
4038
|
+
}
|
|
4039
|
+
destroy(options) {
|
|
4040
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4041
|
+
super.destroy(options);
|
|
4042
|
+
}
|
|
3397
4043
|
}
|
|
3398
4044
|
|
|
3399
4045
|
/**
|
|
3400
4046
|
* Win amount display with countup animation.
|
|
3401
4047
|
*
|
|
3402
4048
|
* Shows a dramatic countup from 0 to the win amount, with optional
|
|
3403
|
-
* scale pop effect — typical of slot games.
|
|
4049
|
+
* scale pop effect — typical of slot games. Uses engine Tween system.
|
|
3404
4050
|
*
|
|
3405
4051
|
* @example
|
|
3406
4052
|
* ```ts
|
|
@@ -3411,9 +4057,11 @@ class BalanceDisplay extends Container {
|
|
|
3411
4057
|
* ```
|
|
3412
4058
|
*/
|
|
3413
4059
|
class WinDisplay extends Container {
|
|
4060
|
+
__uiComponent = true;
|
|
3414
4061
|
_label;
|
|
3415
4062
|
_config;
|
|
3416
|
-
|
|
4063
|
+
/** Internal target for Tween countup */
|
|
4064
|
+
_tweenTarget = { value: 0 };
|
|
3417
4065
|
constructor(config = {}) {
|
|
3418
4066
|
super();
|
|
3419
4067
|
this._config = {
|
|
@@ -3443,47 +4091,30 @@ class WinDisplay extends Container {
|
|
|
3443
4091
|
*/
|
|
3444
4092
|
async showWin(amount) {
|
|
3445
4093
|
this.visible = true;
|
|
3446
|
-
this._cancelCountup = false;
|
|
3447
4094
|
this.alpha = 1;
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
4095
|
+
// Cancel any running animation
|
|
4096
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4097
|
+
Tween.killTweensOf(this);
|
|
4098
|
+
// Setup countup
|
|
4099
|
+
this._tweenTarget.value = 0;
|
|
3451
4100
|
this.scale.set(0.5);
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
return;
|
|
3458
|
-
}
|
|
3459
|
-
const elapsed = Date.now() - startTime;
|
|
3460
|
-
const t = Math.min(elapsed / duration, 1);
|
|
3461
|
-
const eased = Easing.easeOutCubic(t);
|
|
3462
|
-
// Countup
|
|
3463
|
-
const current = amount * eased;
|
|
3464
|
-
this.displayAmount(current);
|
|
3465
|
-
// Scale animation
|
|
3466
|
-
const scaleT = Math.min(elapsed / 300, 1);
|
|
3467
|
-
const scaleEased = Easing.easeOutBack(scaleT);
|
|
3468
|
-
const targetScale = 1;
|
|
3469
|
-
this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
|
|
3470
|
-
if (t < 1) {
|
|
3471
|
-
requestAnimationFrame(tick);
|
|
3472
|
-
}
|
|
3473
|
-
else {
|
|
3474
|
-
this.displayAmount(amount);
|
|
3475
|
-
this.scale.set(1);
|
|
3476
|
-
resolve();
|
|
3477
|
-
}
|
|
3478
|
-
};
|
|
3479
|
-
requestAnimationFrame(tick);
|
|
4101
|
+
// Scale pop animation
|
|
4102
|
+
const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
|
|
4103
|
+
// Countup animation
|
|
4104
|
+
const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
|
|
4105
|
+
this.displayAmount(this._tweenTarget.value);
|
|
3480
4106
|
});
|
|
4107
|
+
await Promise.all([scalePromise, countupPromise]);
|
|
4108
|
+
// Ensure final value is exact
|
|
4109
|
+
this.displayAmount(amount);
|
|
4110
|
+
this.scale.set(1);
|
|
3481
4111
|
}
|
|
3482
4112
|
/**
|
|
3483
4113
|
* Skip the countup animation and show the final amount immediately.
|
|
3484
4114
|
*/
|
|
3485
4115
|
skipCountup(amount) {
|
|
3486
|
-
this.
|
|
4116
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4117
|
+
Tween.killTweensOf(this);
|
|
3487
4118
|
this.displayAmount(amount);
|
|
3488
4119
|
this.scale.set(1);
|
|
3489
4120
|
}
|
|
@@ -3491,19 +4122,33 @@ class WinDisplay extends Container {
|
|
|
3491
4122
|
* Hide the win display.
|
|
3492
4123
|
*/
|
|
3493
4124
|
hide() {
|
|
4125
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4126
|
+
Tween.killTweensOf(this);
|
|
3494
4127
|
this.visible = false;
|
|
3495
4128
|
this._label.text = '';
|
|
3496
4129
|
}
|
|
3497
4130
|
displayAmount(amount) {
|
|
3498
4131
|
this._label.setCurrency(amount, this._config.currency, this._config.locale);
|
|
3499
4132
|
}
|
|
4133
|
+
/** React reconciler update hook */
|
|
4134
|
+
updateConfig(changed) {
|
|
4135
|
+
if ('currency' in changed)
|
|
4136
|
+
this._config.currency = changed.currency;
|
|
4137
|
+
if ('locale' in changed)
|
|
4138
|
+
this._config.locale = changed.locale;
|
|
4139
|
+
}
|
|
4140
|
+
destroy(options) {
|
|
4141
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4142
|
+
Tween.killTweensOf(this);
|
|
4143
|
+
super.destroy(options);
|
|
4144
|
+
}
|
|
3500
4145
|
}
|
|
3501
4146
|
|
|
3502
4147
|
/**
|
|
3503
4148
|
* Modal overlay component.
|
|
3504
4149
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3505
4150
|
*
|
|
3506
|
-
*
|
|
4151
|
+
* Content is automatically centered via position calculations.
|
|
3507
4152
|
*
|
|
3508
4153
|
* @example
|
|
3509
4154
|
* ```ts
|
|
@@ -3514,6 +4159,7 @@ class WinDisplay extends Container {
|
|
|
3514
4159
|
* ```
|
|
3515
4160
|
*/
|
|
3516
4161
|
class Modal extends Container {
|
|
4162
|
+
__uiComponent = true;
|
|
3517
4163
|
_overlay;
|
|
3518
4164
|
_contentContainer;
|
|
3519
4165
|
_config;
|
|
@@ -3583,6 +4229,17 @@ class Modal extends Container {
|
|
|
3583
4229
|
this._showing = false;
|
|
3584
4230
|
this.onClose?.();
|
|
3585
4231
|
}
|
|
4232
|
+
/** React reconciler update hook */
|
|
4233
|
+
updateConfig(changed) {
|
|
4234
|
+
if ('overlayAlpha' in changed)
|
|
4235
|
+
this._config.overlayAlpha = changed.overlayAlpha;
|
|
4236
|
+
if ('closeOnOverlay' in changed)
|
|
4237
|
+
this._config.closeOnOverlay = changed.closeOnOverlay;
|
|
4238
|
+
if ('animationDuration' in changed)
|
|
4239
|
+
this._config.animationDuration = changed.animationDuration;
|
|
4240
|
+
if ('onClose' in changed)
|
|
4241
|
+
this.onClose = changed.onClose;
|
|
4242
|
+
}
|
|
3586
4243
|
}
|
|
3587
4244
|
|
|
3588
4245
|
const TOAST_COLORS = {
|
|
@@ -3602,17 +4259,21 @@ const TOAST_COLORS = {
|
|
|
3602
4259
|
* ```
|
|
3603
4260
|
*/
|
|
3604
4261
|
class Toast extends Container {
|
|
4262
|
+
__uiComponent = true;
|
|
3605
4263
|
_bg;
|
|
4264
|
+
_customBg;
|
|
3606
4265
|
_text;
|
|
3607
4266
|
_config;
|
|
3608
|
-
|
|
4267
|
+
_dismissPending = false;
|
|
3609
4268
|
constructor(config = {}) {
|
|
3610
4269
|
super();
|
|
3611
4270
|
this._config = {
|
|
3612
4271
|
duration: config.duration ?? 3000,
|
|
3613
4272
|
bottomOffset: config.bottomOffset ?? 60,
|
|
3614
4273
|
};
|
|
3615
|
-
|
|
4274
|
+
const customBg = resolveView(config.backgroundView);
|
|
4275
|
+
this._customBg = !!customBg;
|
|
4276
|
+
this._bg = customBg ?? new Graphics();
|
|
3616
4277
|
this.addChild(this._bg);
|
|
3617
4278
|
this._text = new Text({
|
|
3618
4279
|
text: '',
|
|
@@ -3630,18 +4291,27 @@ class Toast extends Container {
|
|
|
3630
4291
|
* Show a toast message.
|
|
3631
4292
|
*/
|
|
3632
4293
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
4294
|
+
// Cancel any pending dismiss
|
|
4295
|
+
Tween.killTweensOf(this);
|
|
4296
|
+
this._dismissPending = false;
|
|
3636
4297
|
this._text.text = message;
|
|
3637
4298
|
const padding = 20;
|
|
3638
4299
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3639
4300
|
const height = 44;
|
|
3640
4301
|
const radius = 8;
|
|
3641
4302
|
// Draw the background
|
|
3642
|
-
this.
|
|
3643
|
-
|
|
3644
|
-
|
|
4303
|
+
if (this._customBg) {
|
|
4304
|
+
this._bg.width = width;
|
|
4305
|
+
this._bg.height = height;
|
|
4306
|
+
this._bg.x = -width / 2;
|
|
4307
|
+
this._bg.y = -height / 2;
|
|
4308
|
+
}
|
|
4309
|
+
else {
|
|
4310
|
+
const g = this._bg;
|
|
4311
|
+
g.clear();
|
|
4312
|
+
g.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
4313
|
+
g.fill(TOAST_COLORS[type]);
|
|
4314
|
+
}
|
|
3645
4315
|
// Position
|
|
3646
4316
|
if (viewWidth && viewHeight) {
|
|
3647
4317
|
this.x = viewWidth / 2;
|
|
@@ -3652,9 +4322,12 @@ class Toast extends Container {
|
|
|
3652
4322
|
this.y += 20;
|
|
3653
4323
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3654
4324
|
if (this._config.duration > 0) {
|
|
3655
|
-
this.
|
|
3656
|
-
|
|
3657
|
-
|
|
4325
|
+
this._dismissPending = true;
|
|
4326
|
+
await Tween.delay(this._config.duration);
|
|
4327
|
+
if (this._dismissPending) {
|
|
4328
|
+
this._dismissPending = false;
|
|
4329
|
+
await this.dismiss();
|
|
4330
|
+
}
|
|
3658
4331
|
}
|
|
3659
4332
|
}
|
|
3660
4333
|
/**
|
|
@@ -3663,57 +4336,36 @@ class Toast extends Container {
|
|
|
3663
4336
|
async dismiss() {
|
|
3664
4337
|
if (!this.visible)
|
|
3665
4338
|
return;
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
this._dismissTimeout = null;
|
|
3669
|
-
}
|
|
4339
|
+
this._dismissPending = false;
|
|
4340
|
+
Tween.killTweensOf(this);
|
|
3670
4341
|
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
3671
4342
|
this.visible = false;
|
|
3672
4343
|
}
|
|
4344
|
+
/** React reconciler update hook */
|
|
4345
|
+
updateConfig(changed) {
|
|
4346
|
+
if ('duration' in changed)
|
|
4347
|
+
this._config.duration = changed.duration;
|
|
4348
|
+
if ('bottomOffset' in changed)
|
|
4349
|
+
this._config.bottomOffset = changed.bottomOffset;
|
|
4350
|
+
}
|
|
4351
|
+
destroy(options) {
|
|
4352
|
+
this._dismissPending = false;
|
|
4353
|
+
Tween.killTweensOf(this);
|
|
4354
|
+
super.destroy(options);
|
|
4355
|
+
}
|
|
3673
4356
|
}
|
|
3674
4357
|
|
|
3675
4358
|
// ─── Helpers ─────────────────────────────────────────────
|
|
3676
|
-
|
|
3677
|
-
start: 'flex-start',
|
|
3678
|
-
center: 'center',
|
|
3679
|
-
end: 'flex-end',
|
|
3680
|
-
stretch: 'stretch',
|
|
3681
|
-
};
|
|
3682
|
-
function normalizePadding(padding) {
|
|
3683
|
-
if (typeof padding === 'number')
|
|
3684
|
-
return [padding, padding, padding, padding];
|
|
3685
|
-
return padding;
|
|
3686
|
-
}
|
|
3687
|
-
function directionToFlexStyles(direction, maxWidth) {
|
|
4359
|
+
function directionToFlex(direction) {
|
|
3688
4360
|
switch (direction) {
|
|
3689
|
-
case 'horizontal':
|
|
3690
|
-
|
|
3691
|
-
case '
|
|
3692
|
-
|
|
3693
|
-
case 'grid':
|
|
3694
|
-
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
3695
|
-
case 'wrap':
|
|
3696
|
-
return {
|
|
3697
|
-
flexDirection: 'row',
|
|
3698
|
-
flexWrap: 'wrap',
|
|
3699
|
-
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
3700
|
-
};
|
|
4361
|
+
case 'horizontal': return { direction: 'row', wrap: false };
|
|
4362
|
+
case 'vertical': return { direction: 'column', wrap: false };
|
|
4363
|
+
case 'grid': return { direction: 'row', wrap: true };
|
|
4364
|
+
case 'wrap': return { direction: 'row', wrap: true };
|
|
3701
4365
|
}
|
|
3702
4366
|
}
|
|
3703
|
-
function buildLayoutStyles(config) {
|
|
3704
|
-
const [pt, pr, pb, pl] = config.padding;
|
|
3705
|
-
return {
|
|
3706
|
-
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
3707
|
-
gap: config.gap,
|
|
3708
|
-
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
3709
|
-
paddingTop: pt,
|
|
3710
|
-
paddingRight: pr,
|
|
3711
|
-
paddingBottom: pb,
|
|
3712
|
-
paddingLeft: pl,
|
|
3713
|
-
};
|
|
3714
|
-
}
|
|
3715
4367
|
/**
|
|
3716
|
-
* Responsive layout container powered by
|
|
4368
|
+
* Responsive layout container powered by a lightweight built-in flex layout solver.
|
|
3717
4369
|
*
|
|
3718
4370
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3719
4371
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -3740,6 +4392,7 @@ function buildLayoutStyles(config) {
|
|
|
3740
4392
|
* ```
|
|
3741
4393
|
*/
|
|
3742
4394
|
class Layout extends Container {
|
|
4395
|
+
__uiComponent = true;
|
|
3743
4396
|
_layoutConfig;
|
|
3744
4397
|
_padding;
|
|
3745
4398
|
_anchor;
|
|
@@ -3748,6 +4401,7 @@ class Layout extends Container {
|
|
|
3748
4401
|
_items = [];
|
|
3749
4402
|
_viewportWidth = 0;
|
|
3750
4403
|
_viewportHeight = 0;
|
|
4404
|
+
_flex;
|
|
3751
4405
|
constructor(config = {}) {
|
|
3752
4406
|
super();
|
|
3753
4407
|
this._layoutConfig = {
|
|
@@ -3757,7 +4411,7 @@ class Layout extends Container {
|
|
|
3757
4411
|
autoLayout: config.autoLayout ?? true,
|
|
3758
4412
|
columns: config.columns ?? 2,
|
|
3759
4413
|
};
|
|
3760
|
-
this._padding =
|
|
4414
|
+
this._padding = config.padding ?? 0;
|
|
3761
4415
|
this._anchor = config.anchor ?? 'top-left';
|
|
3762
4416
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3763
4417
|
this._breakpoints = config.breakpoints
|
|
@@ -3765,14 +4419,18 @@ class Layout extends Container {
|
|
|
3765
4419
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
3766
4420
|
.sort((a, b) => a[0] - b[0])
|
|
3767
4421
|
: [];
|
|
4422
|
+
// Create internal FlexContainer
|
|
4423
|
+
this._flex = new FlexContainer();
|
|
4424
|
+
super.addChild(this._flex);
|
|
3768
4425
|
this.applyLayoutStyles();
|
|
3769
4426
|
}
|
|
3770
4427
|
/** Add an item to the layout */
|
|
3771
4428
|
addItem(child) {
|
|
3772
4429
|
this._items.push(child);
|
|
3773
|
-
this.
|
|
3774
|
-
|
|
3775
|
-
|
|
4430
|
+
const flexConfig = this.buildFlexItemConfig(child);
|
|
4431
|
+
this._flex.addFlexChild(child, flexConfig);
|
|
4432
|
+
if (this._layoutConfig.autoLayout) {
|
|
4433
|
+
this.applyLayoutStyles();
|
|
3776
4434
|
}
|
|
3777
4435
|
return this;
|
|
3778
4436
|
}
|
|
@@ -3781,15 +4439,13 @@ class Layout extends Container {
|
|
|
3781
4439
|
const idx = this._items.indexOf(child);
|
|
3782
4440
|
if (idx !== -1) {
|
|
3783
4441
|
this._items.splice(idx, 1);
|
|
3784
|
-
this.
|
|
4442
|
+
this._flex.removeFlexChild(child);
|
|
3785
4443
|
}
|
|
3786
4444
|
return this;
|
|
3787
4445
|
}
|
|
3788
4446
|
/** Remove all items */
|
|
3789
4447
|
clearItems() {
|
|
3790
|
-
|
|
3791
|
-
this.removeChild(item);
|
|
3792
|
-
}
|
|
4448
|
+
this._flex.clearFlexChildren();
|
|
3793
4449
|
this._items.length = 0;
|
|
3794
4450
|
return this;
|
|
3795
4451
|
}
|
|
@@ -3812,43 +4468,58 @@ class Layout extends Container {
|
|
|
3812
4468
|
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3813
4469
|
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3814
4470
|
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
3815
|
-
effective.
|
|
3816
|
-
const padding = effective.padding !== undefined
|
|
3817
|
-
? normalizePadding(effective.padding)
|
|
3818
|
-
: this._padding;
|
|
4471
|
+
const padding = effective.padding ?? this._padding;
|
|
3819
4472
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
3820
|
-
const
|
|
3821
|
-
this.
|
|
4473
|
+
const { direction: flexDir, wrap } = directionToFlex(direction);
|
|
4474
|
+
this._flex.setDirection(flexDir);
|
|
4475
|
+
this._flex.setJustifyContent('start');
|
|
4476
|
+
this._flex.setAlignItems(alignment);
|
|
4477
|
+
this._flex.setGap(gap);
|
|
4478
|
+
this._flex.setPadding(padding);
|
|
4479
|
+
// Wrap and maxWidth
|
|
4480
|
+
if (wrap) {
|
|
4481
|
+
this._flex._config.flexWrap = true;
|
|
4482
|
+
if (direction === 'grid' && maxWidth < Infinity) {
|
|
4483
|
+
this._flex._maxWidth = maxWidth;
|
|
4484
|
+
}
|
|
4485
|
+
if (maxWidth < Infinity) {
|
|
4486
|
+
this._flex._maxWidth = maxWidth;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
else {
|
|
4490
|
+
this._flex._config.flexWrap = false;
|
|
4491
|
+
}
|
|
4492
|
+
// Update grid child widths
|
|
3822
4493
|
if (direction === 'grid') {
|
|
3823
4494
|
for (const item of this._items) {
|
|
3824
|
-
this.
|
|
4495
|
+
const flexConfig = this.buildFlexItemConfig(item);
|
|
4496
|
+
item._flexConfig = flexConfig;
|
|
3825
4497
|
}
|
|
3826
4498
|
}
|
|
4499
|
+
// Set explicit size if we have viewport dimensions
|
|
4500
|
+
if (this._viewportWidth > 0 && this._viewportHeight > 0) {
|
|
4501
|
+
this._flex.resize(this._viewportWidth, this._viewportHeight);
|
|
4502
|
+
}
|
|
4503
|
+
else {
|
|
4504
|
+
this._flex.updateLayout();
|
|
4505
|
+
}
|
|
3827
4506
|
}
|
|
3828
|
-
|
|
4507
|
+
buildFlexItemConfig(_child) {
|
|
3829
4508
|
const effective = this.resolveConfig();
|
|
4509
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3830
4510
|
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
const styles = gap > 0
|
|
3836
|
-
? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
|
|
3837
|
-
: { width: `${(100 / columns).toFixed(2)}%` };
|
|
3838
|
-
if (child._layout) {
|
|
3839
|
-
child._layout.setStyle(styles);
|
|
3840
|
-
}
|
|
3841
|
-
else {
|
|
3842
|
-
child.layout = styles;
|
|
4511
|
+
if (direction === 'grid' && columns > 0) {
|
|
4512
|
+
// For grid, give each item a proportional width
|
|
4513
|
+
// The actual pixel width will be computed during layout
|
|
4514
|
+
return { flexGrow: 1 };
|
|
3843
4515
|
}
|
|
4516
|
+
return undefined;
|
|
3844
4517
|
}
|
|
3845
4518
|
applyAnchor() {
|
|
3846
4519
|
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
3847
4520
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3848
4521
|
return;
|
|
3849
|
-
const
|
|
3850
|
-
const contentW = bounds.width * this.scale.x;
|
|
3851
|
-
const contentH = bounds.height * this.scale.y;
|
|
4522
|
+
const { width: contentW, height: contentH } = this._flex.getContentSize();
|
|
3852
4523
|
const vw = this._viewportWidth;
|
|
3853
4524
|
const vh = this._viewportHeight;
|
|
3854
4525
|
let anchorX = 0;
|
|
@@ -3871,8 +4542,8 @@ class Layout extends Container {
|
|
|
3871
4542
|
else {
|
|
3872
4543
|
anchorY = (vh - contentH) / 2;
|
|
3873
4544
|
}
|
|
3874
|
-
this.x = anchorX
|
|
3875
|
-
this.y = anchorY
|
|
4545
|
+
this.x = anchorX;
|
|
4546
|
+
this.y = anchorY;
|
|
3876
4547
|
}
|
|
3877
4548
|
resolveConfig() {
|
|
3878
4549
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
@@ -3885,18 +4556,34 @@ class Layout extends Container {
|
|
|
3885
4556
|
}
|
|
3886
4557
|
return {};
|
|
3887
4558
|
}
|
|
4559
|
+
/** React reconciler update hook */
|
|
4560
|
+
updateConfig(changed) {
|
|
4561
|
+
if ('direction' in changed)
|
|
4562
|
+
this._layoutConfig.direction = changed.direction;
|
|
4563
|
+
if ('gap' in changed)
|
|
4564
|
+
this._layoutConfig.gap = changed.gap;
|
|
4565
|
+
if ('alignment' in changed)
|
|
4566
|
+
this._layoutConfig.alignment = changed.alignment;
|
|
4567
|
+
if ('anchor' in changed)
|
|
4568
|
+
this._anchor = changed.anchor;
|
|
4569
|
+
if ('padding' in changed)
|
|
4570
|
+
this._padding = changed.padding;
|
|
4571
|
+
if ('columns' in changed)
|
|
4572
|
+
this._layoutConfig.columns = changed.columns;
|
|
4573
|
+
this.applyLayoutStyles();
|
|
4574
|
+
if (this._viewportWidth > 0)
|
|
4575
|
+
this.applyAnchor();
|
|
4576
|
+
}
|
|
4577
|
+
destroy(options) {
|
|
4578
|
+
this._items.length = 0;
|
|
4579
|
+
super.destroy(options);
|
|
4580
|
+
}
|
|
3888
4581
|
}
|
|
3889
4582
|
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
horizontal: 'horizontal',
|
|
3893
|
-
both: 'bidirectional',
|
|
3894
|
-
};
|
|
4583
|
+
const DECELERATION = 0.95;
|
|
4584
|
+
const MIN_VELOCITY = 0.5;
|
|
3895
4585
|
/**
|
|
3896
|
-
* Scrollable container
|
|
3897
|
-
*
|
|
3898
|
-
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
3899
|
-
* dynamic rendering optimization for off-screen items.
|
|
4586
|
+
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
3900
4587
|
*
|
|
3901
4588
|
* @example
|
|
3902
4589
|
* ```ts
|
|
@@ -3914,53 +4601,349 @@ const DIRECTION_MAP = {
|
|
|
3914
4601
|
* scene.container.addChild(scroll);
|
|
3915
4602
|
* ```
|
|
3916
4603
|
*/
|
|
3917
|
-
class ScrollContainer extends
|
|
4604
|
+
class ScrollContainer extends Container {
|
|
4605
|
+
__uiComponent = true;
|
|
4606
|
+
_viewport;
|
|
4607
|
+
_internalSetup = true;
|
|
4608
|
+
_content;
|
|
4609
|
+
_maskGfx;
|
|
4610
|
+
_bg = null;
|
|
3918
4611
|
_scrollConfig;
|
|
4612
|
+
_items = [];
|
|
4613
|
+
// Scrollbar
|
|
4614
|
+
_scrollbar = null;
|
|
4615
|
+
_scrollbarConfig;
|
|
4616
|
+
// Drag state
|
|
4617
|
+
_dragging = false;
|
|
4618
|
+
_dragStart = { x: 0, y: 0 };
|
|
4619
|
+
_contentStart = { x: 0, y: 0 };
|
|
4620
|
+
_velocity = { x: 0, y: 0 };
|
|
4621
|
+
_lastDragPos = { x: 0, y: 0 };
|
|
4622
|
+
_lastDragTime = 0;
|
|
4623
|
+
_inertiaActive = false;
|
|
4624
|
+
// Bound handlers for cleanup
|
|
4625
|
+
_onTickBound = null;
|
|
4626
|
+
_onWheelBound = null;
|
|
3919
4627
|
constructor(config) {
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
radius: config.borderRadius ?? 0,
|
|
4628
|
+
super();
|
|
4629
|
+
this._viewport = { width: config.width, height: config.height };
|
|
4630
|
+
this._scrollConfig = {
|
|
4631
|
+
direction: config.direction ?? 'vertical',
|
|
3925
4632
|
elementsMargin: config.elementsMargin ?? 0,
|
|
3926
4633
|
padding: config.padding ?? 0,
|
|
3927
|
-
|
|
4634
|
+
borderRadius: config.borderRadius ?? 0,
|
|
3928
4635
|
disableEasing: config.disableEasing ?? false,
|
|
3929
|
-
globalScroll: config.globalScroll ?? true,
|
|
3930
4636
|
};
|
|
4637
|
+
// Background
|
|
3931
4638
|
if (config.backgroundColor !== undefined) {
|
|
3932
|
-
|
|
4639
|
+
this._bg = new Graphics();
|
|
4640
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
4641
|
+
.fill(config.backgroundColor);
|
|
4642
|
+
this.addChild(this._bg);
|
|
4643
|
+
}
|
|
4644
|
+
// Mask
|
|
4645
|
+
this._maskGfx = new Graphics();
|
|
4646
|
+
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
4647
|
+
.fill(0xffffff);
|
|
4648
|
+
this.addChild(this._maskGfx);
|
|
4649
|
+
// Content container
|
|
4650
|
+
this._content = new Container();
|
|
4651
|
+
this._content.mask = this._maskGfx;
|
|
4652
|
+
this.addChild(this._content);
|
|
4653
|
+
// Interaction
|
|
4654
|
+
this.eventMode = 'static';
|
|
4655
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
4656
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
4657
|
+
this.on('pointermove', this._onPointerMove, this);
|
|
4658
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
4659
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
4660
|
+
// Mouse wheel
|
|
4661
|
+
this._onWheelBound = this._onWheel.bind(this);
|
|
4662
|
+
// Scrollbar
|
|
4663
|
+
const sbWidth = config.scrollbarWidth ?? 6;
|
|
4664
|
+
const sbPadding = config.scrollbarPadding ?? 4;
|
|
4665
|
+
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
4666
|
+
if (config.scrollbar) {
|
|
4667
|
+
const customThumb = resolveView(config.thumbView);
|
|
4668
|
+
if (customThumb) {
|
|
4669
|
+
this._scrollbar = customThumb;
|
|
4670
|
+
}
|
|
4671
|
+
else {
|
|
4672
|
+
const g = new Graphics();
|
|
4673
|
+
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
4674
|
+
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
4675
|
+
this._scrollbar = g;
|
|
4676
|
+
}
|
|
4677
|
+
this._scrollbar.visible = false;
|
|
4678
|
+
super.addChild(this._scrollbar);
|
|
3933
4679
|
}
|
|
3934
|
-
|
|
3935
|
-
this._scrollConfig = config;
|
|
4680
|
+
this._internalSetup = false;
|
|
3936
4681
|
}
|
|
3937
|
-
/**
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4682
|
+
/**
|
|
4683
|
+
* Override addChild so external children are routed to scroll content.
|
|
4684
|
+
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
4685
|
+
*/
|
|
4686
|
+
addChild(...children) {
|
|
4687
|
+
if (this._internalSetup) {
|
|
4688
|
+
return super.addChild(...children);
|
|
4689
|
+
}
|
|
4690
|
+
for (const child of children) {
|
|
4691
|
+
this.addItem(child);
|
|
4692
|
+
}
|
|
4693
|
+
return children[0];
|
|
4694
|
+
}
|
|
4695
|
+
removeChild(...children) {
|
|
4696
|
+
if (this._internalSetup) {
|
|
4697
|
+
return super.removeChild(...children);
|
|
4698
|
+
}
|
|
4699
|
+
for (const child of children) {
|
|
4700
|
+
const idx = this._items.indexOf(child);
|
|
4701
|
+
if (idx !== -1) {
|
|
4702
|
+
this._items.splice(idx, 1);
|
|
4703
|
+
this._content.removeChild(child);
|
|
3944
4704
|
}
|
|
3945
4705
|
}
|
|
3946
|
-
|
|
4706
|
+
this.layoutItems();
|
|
4707
|
+
return children[0];
|
|
4708
|
+
}
|
|
4709
|
+
/** React reconciler update hook */
|
|
4710
|
+
updateConfig(changed) {
|
|
4711
|
+
if ('width' in changed || 'height' in changed) {
|
|
4712
|
+
this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
4716
|
+
enableWheel(canvas) {
|
|
4717
|
+
if (this._onWheelBound) {
|
|
4718
|
+
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
/** Set scrollable content. Replaces any existing items. */
|
|
4722
|
+
setContent(content) {
|
|
4723
|
+
this.clearItems();
|
|
3947
4724
|
const children = [...content.children];
|
|
3948
|
-
|
|
3949
|
-
this.
|
|
4725
|
+
for (const child of children) {
|
|
4726
|
+
this.addItem(child);
|
|
3950
4727
|
}
|
|
3951
4728
|
}
|
|
3952
4729
|
/** Add a single item */
|
|
3953
|
-
addItem(
|
|
3954
|
-
this.
|
|
3955
|
-
|
|
4730
|
+
addItem(child) {
|
|
4731
|
+
this._items.push(child);
|
|
4732
|
+
this._content.addChild(child);
|
|
4733
|
+
this.layoutItems();
|
|
4734
|
+
return this;
|
|
4735
|
+
}
|
|
4736
|
+
/** Remove all items */
|
|
4737
|
+
clearItems() {
|
|
4738
|
+
for (const item of this._items) {
|
|
4739
|
+
this._content.removeChild(item);
|
|
4740
|
+
}
|
|
4741
|
+
this._items.length = 0;
|
|
3956
4742
|
}
|
|
3957
|
-
/**
|
|
4743
|
+
/** Get items */
|
|
4744
|
+
get items() {
|
|
4745
|
+
return this._items;
|
|
4746
|
+
}
|
|
4747
|
+
/** Scroll to make a specific item index visible */
|
|
3958
4748
|
scrollToItem(index) {
|
|
3959
|
-
this.
|
|
4749
|
+
if (index < 0 || index >= this._items.length)
|
|
4750
|
+
return;
|
|
4751
|
+
const item = this._items[index];
|
|
4752
|
+
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
4753
|
+
if (isVert) {
|
|
4754
|
+
this._content.y = -item.y + this._scrollConfig.padding;
|
|
4755
|
+
}
|
|
4756
|
+
else {
|
|
4757
|
+
this._content.x = -item.x + this._scrollConfig.padding;
|
|
4758
|
+
}
|
|
4759
|
+
this.clampScroll();
|
|
3960
4760
|
}
|
|
3961
4761
|
/** Current scroll position */
|
|
3962
4762
|
get scrollPosition() {
|
|
3963
|
-
return { x: this.
|
|
4763
|
+
return { x: this._content.x, y: this._content.y };
|
|
4764
|
+
}
|
|
4765
|
+
/** Resize the scroll viewport */
|
|
4766
|
+
setViewportSize(width, height) {
|
|
4767
|
+
this._viewport.width = width;
|
|
4768
|
+
this._viewport.height = height;
|
|
4769
|
+
this._maskGfx.clear();
|
|
4770
|
+
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
4771
|
+
if (this._bg) {
|
|
4772
|
+
this._bg.clear();
|
|
4773
|
+
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
4774
|
+
.fill(0xffffff); // color will be overridden if needed
|
|
4775
|
+
}
|
|
4776
|
+
this.clampScroll();
|
|
4777
|
+
}
|
|
4778
|
+
// ─── Layout ──────────────────────────────────────────
|
|
4779
|
+
layoutItems() {
|
|
4780
|
+
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
4781
|
+
const isVert = direction !== 'horizontal';
|
|
4782
|
+
let pos = padding;
|
|
4783
|
+
for (const item of this._items) {
|
|
4784
|
+
if (isVert) {
|
|
4785
|
+
item.x = padding;
|
|
4786
|
+
item.y = pos;
|
|
4787
|
+
pos += item.height + elementsMargin;
|
|
4788
|
+
}
|
|
4789
|
+
else {
|
|
4790
|
+
item.x = pos;
|
|
4791
|
+
item.y = padding;
|
|
4792
|
+
pos += item.width + elementsMargin;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
// ─── Drag handling ───────────────────────────────────
|
|
4797
|
+
_onPointerDown(e) {
|
|
4798
|
+
this._dragging = true;
|
|
4799
|
+
this._inertiaActive = false;
|
|
4800
|
+
this._dragStart.x = e.globalX;
|
|
4801
|
+
this._dragStart.y = e.globalY;
|
|
4802
|
+
this._contentStart.x = this._content.x;
|
|
4803
|
+
this._contentStart.y = this._content.y;
|
|
4804
|
+
this._lastDragPos.x = e.globalX;
|
|
4805
|
+
this._lastDragPos.y = e.globalY;
|
|
4806
|
+
this._lastDragTime = Date.now();
|
|
4807
|
+
this._velocity.x = 0;
|
|
4808
|
+
this._velocity.y = 0;
|
|
4809
|
+
this.stopInertia();
|
|
4810
|
+
}
|
|
4811
|
+
_onPointerMove(e) {
|
|
4812
|
+
if (!this._dragging)
|
|
4813
|
+
return;
|
|
4814
|
+
const dx = e.globalX - this._dragStart.x;
|
|
4815
|
+
const dy = e.globalY - this._dragStart.y;
|
|
4816
|
+
const { direction } = this._scrollConfig;
|
|
4817
|
+
if (direction !== 'horizontal') {
|
|
4818
|
+
this._content.y = this._contentStart.y + dy;
|
|
4819
|
+
}
|
|
4820
|
+
if (direction !== 'vertical') {
|
|
4821
|
+
this._content.x = this._contentStart.x + dx;
|
|
4822
|
+
}
|
|
4823
|
+
// Track velocity
|
|
4824
|
+
const now = Date.now();
|
|
4825
|
+
const dt = now - this._lastDragTime;
|
|
4826
|
+
if (dt > 0) {
|
|
4827
|
+
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
4828
|
+
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
4829
|
+
}
|
|
4830
|
+
this._lastDragPos.x = e.globalX;
|
|
4831
|
+
this._lastDragPos.y = e.globalY;
|
|
4832
|
+
this._lastDragTime = now;
|
|
4833
|
+
this.clampScroll();
|
|
4834
|
+
}
|
|
4835
|
+
_onPointerUp() {
|
|
4836
|
+
if (!this._dragging)
|
|
4837
|
+
return;
|
|
4838
|
+
this._dragging = false;
|
|
4839
|
+
if (!this._scrollConfig.disableEasing &&
|
|
4840
|
+
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
4841
|
+
this.startInertia();
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
// ─── Inertia ─────────────────────────────────────────
|
|
4845
|
+
startInertia() {
|
|
4846
|
+
this._inertiaActive = true;
|
|
4847
|
+
this._onTickBound = this._inertiaTick.bind(this);
|
|
4848
|
+
Ticker.shared.add(this._onTickBound);
|
|
4849
|
+
}
|
|
4850
|
+
stopInertia() {
|
|
4851
|
+
if (this._onTickBound && this._inertiaActive) {
|
|
4852
|
+
Ticker.shared.remove(this._onTickBound);
|
|
4853
|
+
this._inertiaActive = false;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
_inertiaTick() {
|
|
4857
|
+
const { direction } = this._scrollConfig;
|
|
4858
|
+
if (direction !== 'horizontal') {
|
|
4859
|
+
this._content.y += this._velocity.y;
|
|
4860
|
+
this._velocity.y *= DECELERATION;
|
|
4861
|
+
}
|
|
4862
|
+
if (direction !== 'vertical') {
|
|
4863
|
+
this._content.x += this._velocity.x;
|
|
4864
|
+
this._velocity.x *= DECELERATION;
|
|
4865
|
+
}
|
|
4866
|
+
this.clampScroll();
|
|
4867
|
+
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
4868
|
+
this.stopInertia();
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
// ─── Mouse wheel ─────────────────────────────────────
|
|
4872
|
+
_onWheel(e) {
|
|
4873
|
+
const { direction } = this._scrollConfig;
|
|
4874
|
+
e.preventDefault();
|
|
4875
|
+
if (direction !== 'horizontal') {
|
|
4876
|
+
this._content.y -= e.deltaY;
|
|
4877
|
+
}
|
|
4878
|
+
if (direction !== 'vertical') {
|
|
4879
|
+
this._content.x -= e.deltaX;
|
|
4880
|
+
}
|
|
4881
|
+
this.clampScroll();
|
|
4882
|
+
}
|
|
4883
|
+
// ─── Scroll bounds ───────────────────────────────────
|
|
4884
|
+
clampScroll() {
|
|
4885
|
+
const { direction } = this._scrollConfig;
|
|
4886
|
+
const bounds = this._content.getLocalBounds();
|
|
4887
|
+
if (direction !== 'horizontal') {
|
|
4888
|
+
const contentHeight = bounds.height + bounds.y;
|
|
4889
|
+
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
4890
|
+
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
4891
|
+
}
|
|
4892
|
+
if (direction !== 'vertical') {
|
|
4893
|
+
const contentWidth = bounds.width + bounds.x;
|
|
4894
|
+
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
4895
|
+
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
4896
|
+
}
|
|
4897
|
+
this.updateScrollbar();
|
|
4898
|
+
}
|
|
4899
|
+
updateScrollbar() {
|
|
4900
|
+
if (!this._scrollbar)
|
|
4901
|
+
return;
|
|
4902
|
+
const { direction } = this._scrollConfig;
|
|
4903
|
+
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
4904
|
+
const bounds = this._content.getLocalBounds();
|
|
4905
|
+
const isVert = direction !== 'horizontal';
|
|
4906
|
+
if (isVert) {
|
|
4907
|
+
const contentH = bounds.height + bounds.y;
|
|
4908
|
+
if (contentH <= this._viewport.height) {
|
|
4909
|
+
this._scrollbar.visible = false;
|
|
4910
|
+
return;
|
|
4911
|
+
}
|
|
4912
|
+
this._scrollbar.visible = true;
|
|
4913
|
+
const ratio = this._viewport.height / contentH;
|
|
4914
|
+
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
4915
|
+
const scrollRange = this._viewport.height - thumbH;
|
|
4916
|
+
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
4917
|
+
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
4918
|
+
this._scrollbar.y = scrollProgress * scrollRange;
|
|
4919
|
+
this._scrollbar.height = thumbH;
|
|
4920
|
+
this._scrollbar.width = sbW;
|
|
4921
|
+
}
|
|
4922
|
+
else {
|
|
4923
|
+
const contentW = bounds.width + bounds.x;
|
|
4924
|
+
if (contentW <= this._viewport.width) {
|
|
4925
|
+
this._scrollbar.visible = false;
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
this._scrollbar.visible = true;
|
|
4929
|
+
const ratio = this._viewport.width / contentW;
|
|
4930
|
+
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
4931
|
+
const scrollRange = this._viewport.width - thumbW;
|
|
4932
|
+
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
4933
|
+
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
4934
|
+
this._scrollbar.x = scrollProgress * scrollRange;
|
|
4935
|
+
this._scrollbar.width = thumbW;
|
|
4936
|
+
this._scrollbar.height = sbW;
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
destroy(options) {
|
|
4940
|
+
this.stopInertia();
|
|
4941
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
4942
|
+
this.off('pointermove', this._onPointerMove, this);
|
|
4943
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
4944
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
4945
|
+
this._items.length = 0;
|
|
4946
|
+
super.destroy(options);
|
|
3964
4947
|
}
|
|
3965
4948
|
}
|
|
3966
4949
|
|
|
@@ -4195,5 +5178,5 @@ class DevBridge {
|
|
|
4195
5178
|
}
|
|
4196
5179
|
}
|
|
4197
5180
|
|
|
4198
|
-
export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, GameApplication, InputManager, Label, Layout, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, ScrollContainer, SpineHelper, SpriteAnimation, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
|
|
5181
|
+
export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, FlexContainer, GameApplication, InputManager, Label, Layout, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, ScrollContainer, SpineHelper, SpriteAnimation, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
|
|
4199
5182
|
//# sourceMappingURL=index.esm.js.map
|