@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/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,449 @@ 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
|
+
// Shrink: if content overflows and mainSize is finite, shrink eligible items
|
|
2969
|
+
if (totalGrow === 0 && mainSize > 0) {
|
|
2970
|
+
const overflow = totalFixed + totalGap - mainSize;
|
|
2971
|
+
if (overflow > 0) {
|
|
2972
|
+
let totalShrinkable = 0;
|
|
2973
|
+
for (const item of items) {
|
|
2974
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
2975
|
+
if (shrink > 0) {
|
|
2976
|
+
totalShrinkable += isRow ? item.w : item.h;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (totalShrinkable > 0) {
|
|
2980
|
+
for (const item of items) {
|
|
2981
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
2982
|
+
if (shrink > 0) {
|
|
2983
|
+
const itemMain = isRow ? item.w : item.h;
|
|
2984
|
+
const reduction = overflow * (itemMain / totalShrinkable);
|
|
2985
|
+
const newSize = Math.max(0, itemMain - reduction);
|
|
2986
|
+
if (isRow) {
|
|
2987
|
+
item.w = newSize;
|
|
2988
|
+
item.child.width = newSize;
|
|
2989
|
+
}
|
|
2990
|
+
else {
|
|
2991
|
+
item.h = newSize;
|
|
2992
|
+
item.child.height = newSize;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
// Calculate total main size after flex
|
|
3000
|
+
let totalMain = totalGap;
|
|
3001
|
+
for (const item of items) {
|
|
3002
|
+
totalMain += isRow ? item.w : item.h;
|
|
3003
|
+
}
|
|
3004
|
+
// Justify: compute starting offset and extra spacing
|
|
3005
|
+
let mainOffset = 0;
|
|
3006
|
+
let extraGap = 0;
|
|
3007
|
+
switch (justify) {
|
|
3008
|
+
case 'start':
|
|
3009
|
+
break;
|
|
3010
|
+
case 'center':
|
|
3011
|
+
mainOffset = Math.max(0, (mainSize - totalMain) / 2);
|
|
3012
|
+
break;
|
|
3013
|
+
case 'end':
|
|
3014
|
+
mainOffset = Math.max(0, mainSize - totalMain);
|
|
3015
|
+
break;
|
|
3016
|
+
case 'space-between':
|
|
3017
|
+
if (items.length > 1) {
|
|
3018
|
+
extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
|
|
3019
|
+
}
|
|
3020
|
+
break;
|
|
3021
|
+
case 'space-around':
|
|
3022
|
+
if (items.length > 0) {
|
|
3023
|
+
const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
|
|
3024
|
+
const segment = totalSpace / items.length;
|
|
3025
|
+
mainOffset = segment / 2;
|
|
3026
|
+
extraGap = segment - gap;
|
|
3027
|
+
}
|
|
3028
|
+
break;
|
|
3029
|
+
}
|
|
3030
|
+
// Position each item
|
|
3031
|
+
let pos = mainOffset;
|
|
3032
|
+
for (const item of items) {
|
|
3033
|
+
const mainDim = isRow ? item.w : item.h;
|
|
3034
|
+
const crossDim = isRow ? item.h : item.w;
|
|
3035
|
+
// Cross-axis alignment (alignSelf overrides align)
|
|
3036
|
+
const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
|
|
3037
|
+
? item.child._flexConfig.alignSelf
|
|
3038
|
+
: align;
|
|
3039
|
+
let crossPos = crossOffset;
|
|
3040
|
+
switch (effectiveAlign) {
|
|
3041
|
+
case 'start':
|
|
3042
|
+
break;
|
|
3043
|
+
case 'center':
|
|
3044
|
+
crossPos += (crossSize - crossDim) / 2;
|
|
3045
|
+
break;
|
|
3046
|
+
case 'end':
|
|
3047
|
+
crossPos += crossSize - crossDim;
|
|
3048
|
+
break;
|
|
3049
|
+
case 'stretch':
|
|
3050
|
+
if (isRow) {
|
|
3051
|
+
item.child.height = crossSize;
|
|
3052
|
+
}
|
|
3053
|
+
else {
|
|
3054
|
+
item.child.width = crossSize;
|
|
3055
|
+
}
|
|
3056
|
+
break;
|
|
3057
|
+
}
|
|
3058
|
+
// Compensate for local bounds offset (e.g. centered anchors)
|
|
3059
|
+
if (isRow) {
|
|
3060
|
+
item.child.x = pos - item.ox;
|
|
3061
|
+
item.child.y = crossPos - item.oy;
|
|
3062
|
+
}
|
|
3063
|
+
else {
|
|
3064
|
+
item.child.x = crossPos - item.ox;
|
|
3065
|
+
item.child.y = pos - item.oy;
|
|
3066
|
+
}
|
|
3067
|
+
pos += mainDim + gap + extraGap;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
// ─── FlexContainer ───────────────────────────────────────
|
|
3071
|
+
/**
|
|
3072
|
+
* Lightweight flexbox-like layout container for PixiJS.
|
|
3073
|
+
*
|
|
3074
|
+
* Supports row/column direction, justify/align, gap, padding, wrapping,
|
|
3075
|
+
* and flex-grow distribution. Zero external dependencies.
|
|
3076
|
+
*
|
|
3077
|
+
* @example
|
|
3078
|
+
* ```ts
|
|
3079
|
+
* const toolbar = new FlexContainer({
|
|
3080
|
+
* direction: 'row',
|
|
3081
|
+
* justifyContent: 'space-between',
|
|
3082
|
+
* alignItems: 'center',
|
|
3083
|
+
* gap: 16,
|
|
3084
|
+
* padding: 12,
|
|
3085
|
+
* });
|
|
3086
|
+
*
|
|
3087
|
+
* toolbar.addFlexChild(button1);
|
|
3088
|
+
* toolbar.addFlexChild(button2);
|
|
3089
|
+
* toolbar.resize(800, 60);
|
|
3090
|
+
* ```
|
|
3091
|
+
*/
|
|
3092
|
+
class FlexContainer extends Container {
|
|
3093
|
+
__uiComponent = true;
|
|
3094
|
+
_config;
|
|
3095
|
+
_padding;
|
|
3096
|
+
_maxWidth;
|
|
3097
|
+
_maxHeight;
|
|
3098
|
+
/** @internal */ _explicitWidth;
|
|
3099
|
+
/** @internal */ _explicitHeight;
|
|
3100
|
+
_layoutChildren = [];
|
|
3101
|
+
_layoutDirty = true;
|
|
3102
|
+
constructor(config = {}) {
|
|
3103
|
+
super();
|
|
3104
|
+
this._config = {
|
|
3105
|
+
direction: config.direction ?? 'row',
|
|
3106
|
+
justifyContent: config.justifyContent ?? 'start',
|
|
3107
|
+
alignItems: config.alignItems ?? 'start',
|
|
3108
|
+
gap: config.gap ?? 0,
|
|
3109
|
+
flexWrap: config.flexWrap ?? false,
|
|
3110
|
+
};
|
|
3111
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
3112
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3113
|
+
this._maxHeight = config.maxHeight ?? Infinity;
|
|
3114
|
+
this._explicitWidth = config.width ?? 0;
|
|
3115
|
+
this._explicitHeight = config.height ?? 0;
|
|
3116
|
+
}
|
|
3117
|
+
// ─── Public API ──────────────────────────────────────
|
|
3118
|
+
/** Add a child with optional flex config. Also registers in flex layout. */
|
|
3119
|
+
addFlexChild(child, flexConfig) {
|
|
3120
|
+
if (flexConfig)
|
|
3121
|
+
child._flexConfig = flexConfig;
|
|
3122
|
+
if (!this._layoutChildren.includes(child)) {
|
|
3123
|
+
this._layoutChildren.push(child);
|
|
3124
|
+
this._layoutDirty = true;
|
|
3125
|
+
}
|
|
3126
|
+
super.addChild(child);
|
|
3127
|
+
return this;
|
|
3128
|
+
}
|
|
3129
|
+
/** Remove a child from flex layout and display list */
|
|
3130
|
+
removeFlexChild(child) {
|
|
3131
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
3132
|
+
if (idx !== -1) {
|
|
3133
|
+
this._layoutChildren.splice(idx, 1);
|
|
3134
|
+
this._layoutDirty = true;
|
|
3135
|
+
}
|
|
3136
|
+
super.removeChild(child);
|
|
3137
|
+
return this;
|
|
3138
|
+
}
|
|
3139
|
+
/** Remove all flex children */
|
|
3140
|
+
clearFlexChildren() {
|
|
3141
|
+
for (const child of this._layoutChildren) {
|
|
3142
|
+
super.removeChild(child);
|
|
3143
|
+
}
|
|
3144
|
+
this._layoutChildren.length = 0;
|
|
3145
|
+
this._layoutDirty = true;
|
|
3146
|
+
return this;
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Override addChild so children automatically participate in flex layout.
|
|
3150
|
+
* This enables declarative usage from React JSX.
|
|
3151
|
+
*/
|
|
3152
|
+
addChild(...children) {
|
|
3153
|
+
for (const child of children) {
|
|
3154
|
+
if (!this._layoutChildren.includes(child)) {
|
|
3155
|
+
this._layoutChildren.push(child);
|
|
3156
|
+
this._layoutDirty = true;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const result = super.addChild(...children);
|
|
3160
|
+
if (this._layoutDirty)
|
|
3161
|
+
this.updateLayout();
|
|
3162
|
+
return result;
|
|
3163
|
+
}
|
|
3164
|
+
removeChild(...children) {
|
|
3165
|
+
for (const child of children) {
|
|
3166
|
+
const idx = this._layoutChildren.indexOf(child);
|
|
3167
|
+
if (idx !== -1) {
|
|
3168
|
+
this._layoutChildren.splice(idx, 1);
|
|
3169
|
+
this._layoutDirty = true;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
return super.removeChild(...children);
|
|
3173
|
+
}
|
|
3174
|
+
/** Get all flex layout children (read-only) */
|
|
3175
|
+
get flexChildren() {
|
|
3176
|
+
return this._layoutChildren;
|
|
3177
|
+
}
|
|
3178
|
+
/** Update the container size and recalculate layout */
|
|
3179
|
+
resize(width, height) {
|
|
3180
|
+
this._explicitWidth = width;
|
|
3181
|
+
this._explicitHeight = height;
|
|
3182
|
+
this._layoutDirty = true;
|
|
3183
|
+
this.updateLayout();
|
|
3184
|
+
}
|
|
3185
|
+
/** Update layout direction */
|
|
3186
|
+
setDirection(direction) {
|
|
3187
|
+
this._config.direction = direction;
|
|
3188
|
+
this._layoutDirty = true;
|
|
3189
|
+
}
|
|
3190
|
+
/** Update justifyContent */
|
|
3191
|
+
setJustifyContent(justify) {
|
|
3192
|
+
this._config.justifyContent = justify;
|
|
3193
|
+
this._layoutDirty = true;
|
|
3194
|
+
}
|
|
3195
|
+
/** Update alignItems */
|
|
3196
|
+
setAlignItems(align) {
|
|
3197
|
+
this._config.alignItems = align;
|
|
3198
|
+
this._layoutDirty = true;
|
|
3199
|
+
}
|
|
3200
|
+
/** Update gap */
|
|
3201
|
+
setGap(gap) {
|
|
3202
|
+
this._config.gap = gap;
|
|
3203
|
+
this._layoutDirty = true;
|
|
3204
|
+
}
|
|
3205
|
+
/** Update padding */
|
|
3206
|
+
setPadding(padding) {
|
|
3207
|
+
this._padding = normalizePadding(padding);
|
|
3208
|
+
this._layoutDirty = true;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Recalculate and apply layout positions for all children.
|
|
3212
|
+
* Called automatically by `resize()`. Call manually after
|
|
3213
|
+
* adding/removing children without resize.
|
|
3214
|
+
*/
|
|
3215
|
+
updateLayout() {
|
|
3216
|
+
this._layoutDirty = false;
|
|
3217
|
+
const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
|
|
3218
|
+
const [pt, pr, pb, pl] = this._padding;
|
|
3219
|
+
const isRow = direction === 'row';
|
|
3220
|
+
const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
|
|
3221
|
+
const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
|
|
3222
|
+
const mainLimit = isRow ? contentW : contentH;
|
|
3223
|
+
const crossLimit = isRow ? contentH : contentW;
|
|
3224
|
+
// Measure children (skip flexExclude — they position themselves)
|
|
3225
|
+
const measured = [];
|
|
3226
|
+
for (const child of this._layoutChildren) {
|
|
3227
|
+
if (child._flexConfig?.flexExclude)
|
|
3228
|
+
continue;
|
|
3229
|
+
const { w, h, ox, oy } = measureChild(child);
|
|
3230
|
+
measured.push({ child, w, h, ox, oy });
|
|
3231
|
+
}
|
|
3232
|
+
// Split into lines (if wrapping)
|
|
3233
|
+
const lines = [];
|
|
3234
|
+
if (flexWrap && mainLimit < Infinity) {
|
|
3235
|
+
let currentLine = [];
|
|
3236
|
+
let lineMain = 0;
|
|
3237
|
+
for (const item of measured) {
|
|
3238
|
+
const itemMain = isRow ? item.w : item.h;
|
|
3239
|
+
const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
|
|
3240
|
+
if (currentLine.length > 0 && wouldBe > mainLimit) {
|
|
3241
|
+
lines.push(currentLine);
|
|
3242
|
+
currentLine = [item];
|
|
3243
|
+
lineMain = itemMain;
|
|
3244
|
+
}
|
|
3245
|
+
else {
|
|
3246
|
+
currentLine.push(item);
|
|
3247
|
+
lineMain = wouldBe;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (currentLine.length > 0)
|
|
3251
|
+
lines.push(currentLine);
|
|
3252
|
+
}
|
|
3253
|
+
else {
|
|
3254
|
+
lines.push(measured);
|
|
3255
|
+
}
|
|
3256
|
+
// Compute cross size per line
|
|
3257
|
+
const lineCrossSizes = lines.map((line) => {
|
|
3258
|
+
let maxCross = 0;
|
|
3259
|
+
for (const item of line) {
|
|
3260
|
+
const cross = isRow ? item.h : item.w;
|
|
3261
|
+
if (cross > maxCross)
|
|
3262
|
+
maxCross = cross;
|
|
3263
|
+
}
|
|
3264
|
+
return maxCross;
|
|
3265
|
+
});
|
|
3266
|
+
// Layout each line
|
|
3267
|
+
let crossOffset = isRow ? pt : pl;
|
|
3268
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3269
|
+
const line = lines[i];
|
|
3270
|
+
const lineCross = lineCrossSizes[i];
|
|
3271
|
+
const mainStart = isRow ? pl : pt;
|
|
3272
|
+
// Offset items by padding
|
|
3273
|
+
const tempItems = line.map((item) => ({ ...item }));
|
|
3274
|
+
// For single-line layouts, use the full available cross space for alignment;
|
|
3275
|
+
// for multi-line (wrapping), each line gets its own measured cross size.
|
|
3276
|
+
const effectiveCross = lines.length === 1 && crossLimit < Infinity
|
|
3277
|
+
? crossLimit
|
|
3278
|
+
: (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
3279
|
+
layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
|
|
3280
|
+
// Apply main-axis padding offset
|
|
3281
|
+
for (const item of tempItems) {
|
|
3282
|
+
const origChild = line.find((l) => l.child === item.child);
|
|
3283
|
+
origChild.child.x = item.child.x + (isRow ? mainStart : 0);
|
|
3284
|
+
origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
|
|
3285
|
+
}
|
|
3286
|
+
crossOffset += lineCross + gap;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
/** Computed content size (after layout) */
|
|
3290
|
+
getContentSize() {
|
|
3291
|
+
if (this._layoutDirty)
|
|
3292
|
+
this.updateLayout();
|
|
3293
|
+
let maxX = 0;
|
|
3294
|
+
let maxY = 0;
|
|
3295
|
+
for (const child of this._layoutChildren) {
|
|
3296
|
+
const { w, h } = measureChild(child);
|
|
3297
|
+
maxX = Math.max(maxX, child.x + w);
|
|
3298
|
+
maxY = Math.max(maxY, child.y + h);
|
|
3299
|
+
}
|
|
3300
|
+
const [, pr, pb] = this._padding;
|
|
3301
|
+
return { width: maxX + pr, height: maxY + pb };
|
|
3302
|
+
}
|
|
3303
|
+
/** React reconciler update hook — applies changed config props */
|
|
3304
|
+
updateConfig(changed) {
|
|
3305
|
+
if ('direction' in changed)
|
|
3306
|
+
this.setDirection(changed.direction);
|
|
3307
|
+
if ('justifyContent' in changed)
|
|
3308
|
+
this.setJustifyContent(changed.justifyContent);
|
|
3309
|
+
if ('alignItems' in changed)
|
|
3310
|
+
this.setAlignItems(changed.alignItems);
|
|
3311
|
+
if ('gap' in changed)
|
|
3312
|
+
this.setGap(changed.gap);
|
|
3313
|
+
if ('padding' in changed)
|
|
3314
|
+
this.setPadding(changed.padding);
|
|
3315
|
+
if ('flexWrap' in changed) {
|
|
3316
|
+
this._config.flexWrap = changed.flexWrap;
|
|
3317
|
+
this._layoutDirty = true;
|
|
3318
|
+
}
|
|
3319
|
+
if ('width' in changed || 'height' in changed) {
|
|
3320
|
+
this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
|
|
3321
|
+
return; // resize calls updateLayout
|
|
3322
|
+
}
|
|
3323
|
+
if (this._layoutDirty)
|
|
3324
|
+
this.updateLayout();
|
|
3325
|
+
}
|
|
3326
|
+
destroy(options) {
|
|
3327
|
+
this._layoutChildren.length = 0;
|
|
3328
|
+
super.destroy(options);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
/**
|
|
3333
|
+
* Resolve a ViewInput to a Container instance.
|
|
3334
|
+
*
|
|
3335
|
+
* @example
|
|
3336
|
+
* ```ts
|
|
3337
|
+
* resolveView('btn-idle') // → Sprite.from('btn-idle')
|
|
3338
|
+
* resolveView(someTexture) // → new Sprite(someTexture)
|
|
3339
|
+
* resolveView(myCustomContainer) // → myCustomContainer (as-is)
|
|
3340
|
+
* resolveView(undefined) // → null
|
|
3341
|
+
* ```
|
|
3342
|
+
*/
|
|
3343
|
+
function resolveView(input) {
|
|
3344
|
+
if (input == null)
|
|
3345
|
+
return null;
|
|
3346
|
+
if (typeof input === 'string')
|
|
3347
|
+
return Sprite.from(input);
|
|
3348
|
+
if (input instanceof Texture)
|
|
3349
|
+
return new Sprite(input);
|
|
3350
|
+
return input;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
2912
3353
|
const DEFAULT_COLORS = {
|
|
2913
3354
|
default: 0xffd700,
|
|
2914
3355
|
hover: 0xffe44d,
|
|
@@ -2917,33 +3358,55 @@ const DEFAULT_COLORS = {
|
|
|
2917
3358
|
};
|
|
2918
3359
|
function makeGraphicsView(w, h, radius, color) {
|
|
2919
3360
|
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 });
|
|
3361
|
+
g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
|
|
2923
3362
|
return g;
|
|
2924
3363
|
}
|
|
2925
3364
|
/**
|
|
2926
|
-
* Interactive button
|
|
3365
|
+
* Interactive button with per-state custom views and animations.
|
|
2927
3366
|
*
|
|
2928
|
-
*
|
|
2929
|
-
*
|
|
3367
|
+
* Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
|
|
3368
|
+
* (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
|
|
3369
|
+
* Falls back to colored Graphics when no custom view is provided.
|
|
2930
3370
|
*
|
|
2931
3371
|
* @example
|
|
2932
3372
|
* ```ts
|
|
3373
|
+
* // Graphics-based (quick prototyping)
|
|
2933
3374
|
* const btn = new Button({
|
|
2934
3375
|
* width: 200, height: 60, borderRadius: 12,
|
|
2935
3376
|
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2936
3377
|
* text: 'SPIN',
|
|
3378
|
+
* onPress: () => spin(),
|
|
3379
|
+
* });
|
|
3380
|
+
*
|
|
3381
|
+
* // Asset-based (production art)
|
|
3382
|
+
* const btn = new Button({
|
|
3383
|
+
* defaultView: 'btn-idle',
|
|
3384
|
+
* hoverView: 'btn-hover',
|
|
3385
|
+
* pressedView: 'btn-pressed',
|
|
3386
|
+
* disabledView: 'btn-disabled',
|
|
3387
|
+
* text: 'SPIN',
|
|
3388
|
+
* onPress: () => spin(),
|
|
2937
3389
|
* });
|
|
2938
3390
|
*
|
|
2939
|
-
*
|
|
2940
|
-
*
|
|
3391
|
+
* // Custom Container view
|
|
3392
|
+
* const btn = new Button({
|
|
3393
|
+
* defaultView: myAnimatedSprite,
|
|
3394
|
+
* text: 'SPIN',
|
|
3395
|
+
* });
|
|
2941
3396
|
* ```
|
|
2942
3397
|
*/
|
|
2943
|
-
class Button extends
|
|
2944
|
-
|
|
3398
|
+
class Button extends Container {
|
|
3399
|
+
__uiComponent = true;
|
|
3400
|
+
_views = new Map();
|
|
3401
|
+
_state = 'default';
|
|
3402
|
+
_enabled = true;
|
|
3403
|
+
_config;
|
|
3404
|
+
_textObj = null;
|
|
3405
|
+
/** Press callback */
|
|
3406
|
+
onPress;
|
|
2945
3407
|
constructor(config = {}) {
|
|
2946
|
-
|
|
3408
|
+
super();
|
|
3409
|
+
this._config = {
|
|
2947
3410
|
width: config.width ?? 200,
|
|
2948
3411
|
height: config.height ?? 60,
|
|
2949
3412
|
borderRadius: config.borderRadius ?? 8,
|
|
@@ -2951,50 +3414,39 @@ class Button extends FancyButton {
|
|
|
2951
3414
|
animationDuration: config.animationDuration ?? 100,
|
|
2952
3415
|
...config,
|
|
2953
3416
|
};
|
|
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
|
-
}
|
|
3417
|
+
this.onPress = config.onPress;
|
|
3418
|
+
this._buildViews(config);
|
|
2988
3419
|
// Text
|
|
2989
3420
|
if (config.text) {
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3421
|
+
this._textObj = new Text({
|
|
3422
|
+
text: config.text,
|
|
3423
|
+
style: {
|
|
3424
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
3425
|
+
fontSize: 20,
|
|
3426
|
+
fill: 0xffffff,
|
|
3427
|
+
fontWeight: 'bold',
|
|
3428
|
+
...config.textStyle,
|
|
3429
|
+
},
|
|
3430
|
+
});
|
|
3431
|
+
this._textObj.anchor.set(0.5);
|
|
3432
|
+
this.addChild(this._textObj);
|
|
3433
|
+
}
|
|
3434
|
+
// Interaction
|
|
3435
|
+
this.eventMode = 'static';
|
|
3436
|
+
this.cursor = 'pointer';
|
|
3437
|
+
this.on('pointerover', this._onPointerOver, this);
|
|
3438
|
+
this.on('pointerout', this._onPointerOut, this);
|
|
3439
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
3440
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
3441
|
+
this.on('pointerupoutside', this._onPointerUpOutside, this);
|
|
2994
3442
|
if (config.disabled) {
|
|
2995
3443
|
this.enabled = false;
|
|
2996
3444
|
}
|
|
2997
3445
|
}
|
|
3446
|
+
/** Current button state */
|
|
3447
|
+
get state() {
|
|
3448
|
+
return this._state;
|
|
3449
|
+
}
|
|
2998
3450
|
/** Enable the button */
|
|
2999
3451
|
enable() {
|
|
3000
3452
|
this.enabled = true;
|
|
@@ -3003,29 +3455,161 @@ class Button extends FancyButton {
|
|
|
3003
3455
|
disable() {
|
|
3004
3456
|
this.enabled = false;
|
|
3005
3457
|
}
|
|
3458
|
+
/** Whether the button is enabled */
|
|
3459
|
+
get enabled() {
|
|
3460
|
+
return this._enabled;
|
|
3461
|
+
}
|
|
3462
|
+
set enabled(value) {
|
|
3463
|
+
this._enabled = value;
|
|
3464
|
+
this.cursor = value ? 'pointer' : 'default';
|
|
3465
|
+
this.eventMode = value ? 'static' : 'none';
|
|
3466
|
+
this._setState(value ? 'default' : 'disabled');
|
|
3467
|
+
}
|
|
3006
3468
|
/** Whether the button is disabled */
|
|
3007
3469
|
get disabled() {
|
|
3008
|
-
return !this.
|
|
3470
|
+
return !this._enabled;
|
|
3471
|
+
}
|
|
3472
|
+
/** Update button text */
|
|
3473
|
+
set text(value) {
|
|
3474
|
+
if (this._textObj) {
|
|
3475
|
+
this._textObj.text = value;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
// ─── View building ──────────────────────────────────
|
|
3479
|
+
_buildViews(config) {
|
|
3480
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
3481
|
+
const { width, height, borderRadius } = this._config;
|
|
3482
|
+
const stateViews = {
|
|
3483
|
+
default: config.defaultView,
|
|
3484
|
+
hover: config.hoverView,
|
|
3485
|
+
pressed: config.pressedView,
|
|
3486
|
+
disabled: config.disabledView,
|
|
3487
|
+
};
|
|
3488
|
+
const states = ['default', 'hover', 'pressed', 'disabled'];
|
|
3489
|
+
for (const state of states) {
|
|
3490
|
+
const customView = resolveView(stateViews[state]);
|
|
3491
|
+
const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
|
|
3492
|
+
view.visible = state === 'default';
|
|
3493
|
+
this._views.set(state, view);
|
|
3494
|
+
this.addChild(view);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
_rebuildViews() {
|
|
3498
|
+
for (const [, view] of this._views) {
|
|
3499
|
+
this.removeChild(view);
|
|
3500
|
+
view.destroy();
|
|
3501
|
+
}
|
|
3502
|
+
this._views.clear();
|
|
3503
|
+
this._buildViews(this._config);
|
|
3504
|
+
// Re-insert views before text
|
|
3505
|
+
if (this._textObj && this._textObj.parent === this) {
|
|
3506
|
+
this.setChildIndex(this._textObj, this.children.length - 1);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
// ─── State management ───────────────────────────────
|
|
3510
|
+
_setState(state) {
|
|
3511
|
+
if (this._state === state)
|
|
3512
|
+
return;
|
|
3513
|
+
this._state = state;
|
|
3514
|
+
for (const [s, view] of this._views) {
|
|
3515
|
+
view.visible = s === state;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
_onPointerOver() {
|
|
3519
|
+
if (!this._enabled)
|
|
3520
|
+
return;
|
|
3521
|
+
this._setState('hover');
|
|
3522
|
+
Tween.killTweensOf(this);
|
|
3523
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3524
|
+
}
|
|
3525
|
+
_onPointerOut() {
|
|
3526
|
+
if (!this._enabled)
|
|
3527
|
+
return;
|
|
3528
|
+
this._setState('default');
|
|
3529
|
+
Tween.killTweensOf(this);
|
|
3530
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3531
|
+
}
|
|
3532
|
+
_onPointerDown() {
|
|
3533
|
+
if (!this._enabled)
|
|
3534
|
+
return;
|
|
3535
|
+
this._setState('pressed');
|
|
3536
|
+
Tween.killTweensOf(this);
|
|
3537
|
+
const s = this._config.pressScale;
|
|
3538
|
+
Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3539
|
+
}
|
|
3540
|
+
_onPointerUp() {
|
|
3541
|
+
if (!this._enabled)
|
|
3542
|
+
return;
|
|
3543
|
+
this._setState('hover');
|
|
3544
|
+
Tween.killTweensOf(this);
|
|
3545
|
+
Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3546
|
+
this.onPress?.();
|
|
3547
|
+
}
|
|
3548
|
+
_onPointerUpOutside() {
|
|
3549
|
+
if (!this._enabled)
|
|
3550
|
+
return;
|
|
3551
|
+
this._setState('default');
|
|
3552
|
+
Tween.killTweensOf(this);
|
|
3553
|
+
Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3554
|
+
}
|
|
3555
|
+
/** React reconciler update hook */
|
|
3556
|
+
updateConfig(changed) {
|
|
3557
|
+
if ('text' in changed && this._textObj)
|
|
3558
|
+
this._textObj.text = changed.text;
|
|
3559
|
+
if ('disabled' in changed)
|
|
3560
|
+
this.enabled = !changed.disabled;
|
|
3561
|
+
if ('onPress' in changed)
|
|
3562
|
+
this.onPress = changed.onPress;
|
|
3563
|
+
const structural = [
|
|
3564
|
+
'colors', 'width', 'height', 'borderRadius', 'textStyle',
|
|
3565
|
+
'defaultView', 'hoverView', 'pressedView', 'disabledView',
|
|
3566
|
+
];
|
|
3567
|
+
const needsRebuild = structural.some((k) => k in changed);
|
|
3568
|
+
if (needsRebuild) {
|
|
3569
|
+
Object.assign(this._config, changed);
|
|
3570
|
+
this._rebuildViews();
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
destroy(options) {
|
|
3574
|
+
Tween.killTweensOf(this);
|
|
3575
|
+
this.off('pointerover', this._onPointerOver, this);
|
|
3576
|
+
this.off('pointerout', this._onPointerOut, this);
|
|
3577
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
3578
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
3579
|
+
this.off('pointerupoutside', this._onPointerUpOutside, this);
|
|
3580
|
+
this._views.clear();
|
|
3581
|
+
this._textObj = null;
|
|
3582
|
+
super.destroy(options);
|
|
3009
3583
|
}
|
|
3010
3584
|
}
|
|
3011
3585
|
|
|
3012
|
-
function makeBarGraphics(w, h, radius, color) {
|
|
3013
|
-
return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
3014
|
-
}
|
|
3015
3586
|
/**
|
|
3016
|
-
* Horizontal progress bar
|
|
3587
|
+
* Horizontal progress bar with optional custom track/fill views.
|
|
3017
3588
|
*
|
|
3018
|
-
*
|
|
3589
|
+
* Supports asset-based skinning: provide `trackView` and/or `fillView`
|
|
3590
|
+
* as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
|
|
3591
|
+
* Falls back to colored Graphics when no custom views are provided.
|
|
3019
3592
|
*
|
|
3020
3593
|
* @example
|
|
3021
3594
|
* ```ts
|
|
3595
|
+
* // Graphics-based (quick prototyping)
|
|
3022
3596
|
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3597
|
+
* bar.progress = 0.5;
|
|
3598
|
+
*
|
|
3599
|
+
* // Asset-based (production art)
|
|
3600
|
+
* const bar = new ProgressBar({
|
|
3601
|
+
* width: 300, height: 20,
|
|
3602
|
+
* trackView: 'bar-track',
|
|
3603
|
+
* fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
|
|
3604
|
+
* });
|
|
3605
|
+
* bar.progress = 0.75;
|
|
3025
3606
|
* ```
|
|
3026
3607
|
*/
|
|
3027
3608
|
class ProgressBar extends Container {
|
|
3028
|
-
|
|
3609
|
+
__uiComponent = true;
|
|
3610
|
+
_track;
|
|
3611
|
+
_fill;
|
|
3612
|
+
_fillMask;
|
|
3029
3613
|
_borderGfx;
|
|
3030
3614
|
_config;
|
|
3031
3615
|
_progress = 0;
|
|
@@ -3044,21 +3628,39 @@ class ProgressBar extends Container {
|
|
|
3044
3628
|
animationSpeed: config.animationSpeed ?? 0.1,
|
|
3045
3629
|
};
|
|
3046
3630
|
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
|
-
|
|
3631
|
+
// Track background — custom view or Graphics
|
|
3632
|
+
const customTrack = resolveView(config.trackView);
|
|
3633
|
+
if (customTrack) {
|
|
3634
|
+
customTrack.width = width;
|
|
3635
|
+
customTrack.height = height;
|
|
3636
|
+
this._track = customTrack;
|
|
3637
|
+
}
|
|
3638
|
+
else {
|
|
3639
|
+
const g = new Graphics();
|
|
3640
|
+
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
3641
|
+
this._track = g;
|
|
3642
|
+
}
|
|
3643
|
+
this.addChild(this._track);
|
|
3644
|
+
// Fill bar — custom view or Graphics
|
|
3645
|
+
const customFill = resolveView(config.fillView);
|
|
3646
|
+
if (customFill) {
|
|
3647
|
+
customFill.x = borderWidth;
|
|
3648
|
+
customFill.y = borderWidth;
|
|
3649
|
+
customFill.width = width - borderWidth * 2;
|
|
3650
|
+
customFill.height = height - borderWidth * 2;
|
|
3651
|
+
this._fill = customFill;
|
|
3652
|
+
}
|
|
3653
|
+
else {
|
|
3654
|
+
const g = new Graphics();
|
|
3655
|
+
g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
|
|
3656
|
+
this._fill = g;
|
|
3657
|
+
}
|
|
3658
|
+
this.addChild(this._fill);
|
|
3659
|
+
// Mask for the fill (controls visible width)
|
|
3660
|
+
this._fillMask = new Graphics();
|
|
3661
|
+
this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
|
|
3662
|
+
this.addChild(this._fillMask);
|
|
3663
|
+
this._fill.mask = this._fillMask;
|
|
3062
3664
|
// Border overlay
|
|
3063
3665
|
this._borderGfx = new Graphics();
|
|
3064
3666
|
if (borderColor !== undefined && borderWidth > 0) {
|
|
@@ -3076,7 +3678,7 @@ class ProgressBar extends Container {
|
|
|
3076
3678
|
this._progress = Math.max(0, Math.min(1, value));
|
|
3077
3679
|
if (!this._config.animated) {
|
|
3078
3680
|
this._displayedProgress = this._progress;
|
|
3079
|
-
this.
|
|
3681
|
+
this.updateMask();
|
|
3080
3682
|
}
|
|
3081
3683
|
}
|
|
3082
3684
|
/**
|
|
@@ -3087,11 +3689,26 @@ class ProgressBar extends Container {
|
|
|
3087
3689
|
return;
|
|
3088
3690
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
3089
3691
|
this._displayedProgress = this._progress;
|
|
3692
|
+
this.updateMask();
|
|
3090
3693
|
return;
|
|
3091
3694
|
}
|
|
3092
3695
|
this._displayedProgress +=
|
|
3093
3696
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
3094
|
-
this.
|
|
3697
|
+
this.updateMask();
|
|
3698
|
+
}
|
|
3699
|
+
/** React reconciler update hook */
|
|
3700
|
+
updateConfig(changed) {
|
|
3701
|
+
if ('progress' in changed)
|
|
3702
|
+
this.progress = changed.progress;
|
|
3703
|
+
if ('animated' in changed)
|
|
3704
|
+
this._config.animated = changed.animated;
|
|
3705
|
+
if ('animationSpeed' in changed)
|
|
3706
|
+
this._config.animationSpeed = changed.animationSpeed;
|
|
3707
|
+
}
|
|
3708
|
+
updateMask() {
|
|
3709
|
+
const w = this._config.width * this._displayedProgress;
|
|
3710
|
+
this._fillMask.clear();
|
|
3711
|
+
this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
|
|
3095
3712
|
}
|
|
3096
3713
|
}
|
|
3097
3714
|
|
|
@@ -3109,6 +3726,7 @@ class ProgressBar extends Container {
|
|
|
3109
3726
|
* ```
|
|
3110
3727
|
*/
|
|
3111
3728
|
class Label extends Container {
|
|
3729
|
+
__uiComponent = true;
|
|
3112
3730
|
_text;
|
|
3113
3731
|
_maxWidth;
|
|
3114
3732
|
_autoFit;
|
|
@@ -3175,6 +3793,21 @@ class Label extends Container {
|
|
|
3175
3793
|
maximumFractionDigits: decimals,
|
|
3176
3794
|
}).format(value);
|
|
3177
3795
|
}
|
|
3796
|
+
/** React reconciler update hook */
|
|
3797
|
+
updateConfig(changed) {
|
|
3798
|
+
if ('text' in changed)
|
|
3799
|
+
this.text = changed.text;
|
|
3800
|
+
if ('maxWidth' in changed)
|
|
3801
|
+
this.maxWidth = changed.maxWidth;
|
|
3802
|
+
if ('autoFit' in changed) {
|
|
3803
|
+
this._autoFit = changed.autoFit;
|
|
3804
|
+
this.fitText();
|
|
3805
|
+
}
|
|
3806
|
+
if ('style' in changed && typeof changed.style === 'object') {
|
|
3807
|
+
Object.assign(this._text.style, changed.style);
|
|
3808
|
+
this.fitText();
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3178
3811
|
fitText() {
|
|
3179
3812
|
if (!this._autoFit || this._maxWidth === Infinity)
|
|
3180
3813
|
return;
|
|
@@ -3187,10 +3820,10 @@ class Label extends Container {
|
|
|
3187
3820
|
}
|
|
3188
3821
|
|
|
3189
3822
|
/**
|
|
3190
|
-
* Background panel
|
|
3823
|
+
* Background panel with optional flexbox content layout.
|
|
3191
3824
|
*
|
|
3192
3825
|
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
3193
|
-
* Children added
|
|
3826
|
+
* Children added via `addContent()` participate in flex layout automatically.
|
|
3194
3827
|
*
|
|
3195
3828
|
* @example
|
|
3196
3829
|
* ```ts
|
|
@@ -3205,9 +3838,14 @@ class Label extends Container {
|
|
|
3205
3838
|
* });
|
|
3206
3839
|
* ```
|
|
3207
3840
|
*/
|
|
3208
|
-
class Panel extends
|
|
3841
|
+
class Panel extends Container {
|
|
3842
|
+
__uiComponent = true;
|
|
3843
|
+
_bg;
|
|
3844
|
+
_content;
|
|
3845
|
+
_internalSetup = true;
|
|
3209
3846
|
_panelConfig;
|
|
3210
3847
|
constructor(config = {}) {
|
|
3848
|
+
super();
|
|
3211
3849
|
const resolvedConfig = {
|
|
3212
3850
|
width: config.width ?? 400,
|
|
3213
3851
|
height: config.height ?? 300,
|
|
@@ -3215,8 +3853,8 @@ class Panel extends LayoutContainer {
|
|
|
3215
3853
|
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
3216
3854
|
...config,
|
|
3217
3855
|
};
|
|
3218
|
-
|
|
3219
|
-
|
|
3856
|
+
this._panelConfig = resolvedConfig;
|
|
3857
|
+
// Create background
|
|
3220
3858
|
if (config.nineSliceTexture) {
|
|
3221
3859
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
3222
3860
|
? Texture.from(config.nineSliceTexture)
|
|
@@ -3232,40 +3870,102 @@ class Panel extends LayoutContainer {
|
|
|
3232
3870
|
nineSlice.width = resolvedConfig.width;
|
|
3233
3871
|
nineSlice.height = resolvedConfig.height;
|
|
3234
3872
|
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
3235
|
-
|
|
3873
|
+
this._bg = nineSlice;
|
|
3236
3874
|
}
|
|
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;
|
|
3875
|
+
else {
|
|
3876
|
+
const g = new Graphics();
|
|
3877
|
+
const bgColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3878
|
+
const radius = config.borderRadius ?? 0;
|
|
3879
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
|
|
3250
3880
|
if (config.borderColor !== undefined && config.borderWidth) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3881
|
+
g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
|
|
3882
|
+
.stroke({ color: config.borderColor, width: config.borderWidth });
|
|
3253
3883
|
}
|
|
3884
|
+
g.alpha = resolvedConfig.backgroundAlpha;
|
|
3885
|
+
this._bg = g;
|
|
3254
3886
|
}
|
|
3255
|
-
this.
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3887
|
+
this.addChild(this._bg);
|
|
3888
|
+
// Create content flex container
|
|
3889
|
+
this._content = new FlexContainer({
|
|
3890
|
+
...config.layout,
|
|
3891
|
+
direction: config.layout?.direction ?? 'column',
|
|
3892
|
+
justifyContent: config.layout?.justifyContent ?? 'start',
|
|
3893
|
+
alignItems: config.layout?.alignItems ?? 'start',
|
|
3894
|
+
gap: config.layout?.gap ?? 0,
|
|
3895
|
+
padding: resolvedConfig.padding,
|
|
3896
|
+
width: resolvedConfig.width,
|
|
3897
|
+
height: resolvedConfig.height,
|
|
3898
|
+
});
|
|
3899
|
+
this.addChild(this._content);
|
|
3900
|
+
this._internalSetup = false;
|
|
3259
3901
|
}
|
|
3260
|
-
/** Access the content container
|
|
3902
|
+
/** Access the content flex container — add children here for layout */
|
|
3261
3903
|
get content() {
|
|
3262
|
-
return this.
|
|
3904
|
+
return this._content;
|
|
3905
|
+
}
|
|
3906
|
+
/** Convenience: add a child to the content layout */
|
|
3907
|
+
addContent(child) {
|
|
3908
|
+
this._content.addFlexChild(child);
|
|
3909
|
+
this._content.updateLayout();
|
|
3910
|
+
return this;
|
|
3263
3911
|
}
|
|
3264
3912
|
/** Resize the panel */
|
|
3265
3913
|
setSize(width, height) {
|
|
3266
3914
|
this._panelConfig.width = width;
|
|
3267
3915
|
this._panelConfig.height = height;
|
|
3268
|
-
|
|
3916
|
+
// Resize background
|
|
3917
|
+
if (this._bg instanceof NineSliceSprite) {
|
|
3918
|
+
this._bg.width = width;
|
|
3919
|
+
this._bg.height = height;
|
|
3920
|
+
}
|
|
3921
|
+
else if (this._bg instanceof Graphics) {
|
|
3922
|
+
const radius = this._panelConfig.borderRadius ?? 0;
|
|
3923
|
+
const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
|
|
3924
|
+
this._bg.clear();
|
|
3925
|
+
this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
|
|
3926
|
+
if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
|
|
3927
|
+
this._bg.roundRect(0, 0, width, height, radius)
|
|
3928
|
+
.stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
|
|
3929
|
+
}
|
|
3930
|
+
this._bg.alpha = this._panelConfig.backgroundAlpha;
|
|
3931
|
+
}
|
|
3932
|
+
this._content.resize(width, height);
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* Override addChild so external children are routed to content FlexContainer.
|
|
3936
|
+
* Enables `<panel><label /><button /></panel>` in React JSX.
|
|
3937
|
+
*/
|
|
3938
|
+
addChild(...children) {
|
|
3939
|
+
if (this._internalSetup) {
|
|
3940
|
+
return super.addChild(...children);
|
|
3941
|
+
}
|
|
3942
|
+
for (const child of children) {
|
|
3943
|
+
this._content.addFlexChild(child);
|
|
3944
|
+
}
|
|
3945
|
+
this._content.updateLayout();
|
|
3946
|
+
return children[0];
|
|
3947
|
+
}
|
|
3948
|
+
removeChild(...children) {
|
|
3949
|
+
if (this._internalSetup) {
|
|
3950
|
+
return super.removeChild(...children);
|
|
3951
|
+
}
|
|
3952
|
+
for (const child of children) {
|
|
3953
|
+
this._content.removeFlexChild(child);
|
|
3954
|
+
}
|
|
3955
|
+
return children[0];
|
|
3956
|
+
}
|
|
3957
|
+
/** React reconciler update hook */
|
|
3958
|
+
updateConfig(changed) {
|
|
3959
|
+
if ('width' in changed || 'height' in changed) {
|
|
3960
|
+
this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
|
|
3961
|
+
}
|
|
3962
|
+
if ('backgroundAlpha' in changed) {
|
|
3963
|
+
this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
|
|
3964
|
+
this._bg.alpha = changed.backgroundAlpha;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
destroy(options) {
|
|
3968
|
+
super.destroy(options);
|
|
3269
3969
|
}
|
|
3270
3970
|
}
|
|
3271
3971
|
|
|
@@ -3273,7 +3973,7 @@ class Panel extends LayoutContainer {
|
|
|
3273
3973
|
* Reactive balance display component.
|
|
3274
3974
|
*
|
|
3275
3975
|
* Automatically formats currency and can animate value changes
|
|
3276
|
-
* with a smooth countup/countdown effect.
|
|
3976
|
+
* with a smooth countup/countdown effect using engine Tween.
|
|
3277
3977
|
*
|
|
3278
3978
|
* @example
|
|
3279
3979
|
* ```ts
|
|
@@ -3285,13 +3985,14 @@ class Panel extends LayoutContainer {
|
|
|
3285
3985
|
* ```
|
|
3286
3986
|
*/
|
|
3287
3987
|
class BalanceDisplay extends Container {
|
|
3988
|
+
__uiComponent = true;
|
|
3288
3989
|
_prefixLabel = null;
|
|
3289
3990
|
_valueLabel;
|
|
3290
3991
|
_config;
|
|
3291
3992
|
_currentValue = 0;
|
|
3292
3993
|
_displayedValue = 0;
|
|
3293
|
-
|
|
3294
|
-
|
|
3994
|
+
/** Internal target for Tween animation */
|
|
3995
|
+
_tweenTarget = { value: 0 };
|
|
3295
3996
|
constructor(config = {}) {
|
|
3296
3997
|
super();
|
|
3297
3998
|
this._config = {
|
|
@@ -3352,37 +4053,13 @@ class BalanceDisplay extends Container {
|
|
|
3352
4053
|
this._config.currency = currency;
|
|
3353
4054
|
this.updateDisplay();
|
|
3354
4055
|
}
|
|
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);
|
|
4056
|
+
animateValue(from, to) {
|
|
4057
|
+
// Cancel any running animation
|
|
4058
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4059
|
+
this._tweenTarget.value = from;
|
|
4060
|
+
Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
|
|
4061
|
+
this._displayedValue = this._tweenTarget.value;
|
|
4062
|
+
this.updateDisplay();
|
|
3386
4063
|
});
|
|
3387
4064
|
}
|
|
3388
4065
|
updateDisplay() {
|
|
@@ -3394,13 +4071,24 @@ class BalanceDisplay extends Container {
|
|
|
3394
4071
|
this._valueLabel.y = 14;
|
|
3395
4072
|
}
|
|
3396
4073
|
}
|
|
4074
|
+
/** React reconciler update hook */
|
|
4075
|
+
updateConfig(changed) {
|
|
4076
|
+
if ('value' in changed)
|
|
4077
|
+
this.setValue(changed.value);
|
|
4078
|
+
if ('currency' in changed)
|
|
4079
|
+
this.setCurrency(changed.currency);
|
|
4080
|
+
}
|
|
4081
|
+
destroy(options) {
|
|
4082
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4083
|
+
super.destroy(options);
|
|
4084
|
+
}
|
|
3397
4085
|
}
|
|
3398
4086
|
|
|
3399
4087
|
/**
|
|
3400
4088
|
* Win amount display with countup animation.
|
|
3401
4089
|
*
|
|
3402
4090
|
* Shows a dramatic countup from 0 to the win amount, with optional
|
|
3403
|
-
* scale pop effect — typical of slot games.
|
|
4091
|
+
* scale pop effect — typical of slot games. Uses engine Tween system.
|
|
3404
4092
|
*
|
|
3405
4093
|
* @example
|
|
3406
4094
|
* ```ts
|
|
@@ -3411,9 +4099,11 @@ class BalanceDisplay extends Container {
|
|
|
3411
4099
|
* ```
|
|
3412
4100
|
*/
|
|
3413
4101
|
class WinDisplay extends Container {
|
|
4102
|
+
__uiComponent = true;
|
|
3414
4103
|
_label;
|
|
3415
4104
|
_config;
|
|
3416
|
-
|
|
4105
|
+
/** Internal target for Tween countup */
|
|
4106
|
+
_tweenTarget = { value: 0 };
|
|
3417
4107
|
constructor(config = {}) {
|
|
3418
4108
|
super();
|
|
3419
4109
|
this._config = {
|
|
@@ -3443,47 +4133,30 @@ class WinDisplay extends Container {
|
|
|
3443
4133
|
*/
|
|
3444
4134
|
async showWin(amount) {
|
|
3445
4135
|
this.visible = true;
|
|
3446
|
-
this._cancelCountup = false;
|
|
3447
4136
|
this.alpha = 1;
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
4137
|
+
// Cancel any running animation
|
|
4138
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4139
|
+
Tween.killTweensOf(this);
|
|
4140
|
+
// Setup countup
|
|
4141
|
+
this._tweenTarget.value = 0;
|
|
3451
4142
|
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);
|
|
4143
|
+
// Scale pop animation
|
|
4144
|
+
const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
|
|
4145
|
+
// Countup animation
|
|
4146
|
+
const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
|
|
4147
|
+
this.displayAmount(this._tweenTarget.value);
|
|
3480
4148
|
});
|
|
4149
|
+
await Promise.all([scalePromise, countupPromise]);
|
|
4150
|
+
// Ensure final value is exact
|
|
4151
|
+
this.displayAmount(amount);
|
|
4152
|
+
this.scale.set(1);
|
|
3481
4153
|
}
|
|
3482
4154
|
/**
|
|
3483
4155
|
* Skip the countup animation and show the final amount immediately.
|
|
3484
4156
|
*/
|
|
3485
4157
|
skipCountup(amount) {
|
|
3486
|
-
this.
|
|
4158
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4159
|
+
Tween.killTweensOf(this);
|
|
3487
4160
|
this.displayAmount(amount);
|
|
3488
4161
|
this.scale.set(1);
|
|
3489
4162
|
}
|
|
@@ -3491,19 +4164,33 @@ class WinDisplay extends Container {
|
|
|
3491
4164
|
* Hide the win display.
|
|
3492
4165
|
*/
|
|
3493
4166
|
hide() {
|
|
4167
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4168
|
+
Tween.killTweensOf(this);
|
|
3494
4169
|
this.visible = false;
|
|
3495
4170
|
this._label.text = '';
|
|
3496
4171
|
}
|
|
3497
4172
|
displayAmount(amount) {
|
|
3498
4173
|
this._label.setCurrency(amount, this._config.currency, this._config.locale);
|
|
3499
4174
|
}
|
|
4175
|
+
/** React reconciler update hook */
|
|
4176
|
+
updateConfig(changed) {
|
|
4177
|
+
if ('currency' in changed)
|
|
4178
|
+
this._config.currency = changed.currency;
|
|
4179
|
+
if ('locale' in changed)
|
|
4180
|
+
this._config.locale = changed.locale;
|
|
4181
|
+
}
|
|
4182
|
+
destroy(options) {
|
|
4183
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
4184
|
+
Tween.killTweensOf(this);
|
|
4185
|
+
super.destroy(options);
|
|
4186
|
+
}
|
|
3500
4187
|
}
|
|
3501
4188
|
|
|
3502
4189
|
/**
|
|
3503
4190
|
* Modal overlay component.
|
|
3504
4191
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3505
4192
|
*
|
|
3506
|
-
*
|
|
4193
|
+
* Content is automatically centered via position calculations.
|
|
3507
4194
|
*
|
|
3508
4195
|
* @example
|
|
3509
4196
|
* ```ts
|
|
@@ -3514,6 +4201,7 @@ class WinDisplay extends Container {
|
|
|
3514
4201
|
* ```
|
|
3515
4202
|
*/
|
|
3516
4203
|
class Modal extends Container {
|
|
4204
|
+
__uiComponent = true;
|
|
3517
4205
|
_overlay;
|
|
3518
4206
|
_contentContainer;
|
|
3519
4207
|
_config;
|
|
@@ -3583,6 +4271,17 @@ class Modal extends Container {
|
|
|
3583
4271
|
this._showing = false;
|
|
3584
4272
|
this.onClose?.();
|
|
3585
4273
|
}
|
|
4274
|
+
/** React reconciler update hook */
|
|
4275
|
+
updateConfig(changed) {
|
|
4276
|
+
if ('overlayAlpha' in changed)
|
|
4277
|
+
this._config.overlayAlpha = changed.overlayAlpha;
|
|
4278
|
+
if ('closeOnOverlay' in changed)
|
|
4279
|
+
this._config.closeOnOverlay = changed.closeOnOverlay;
|
|
4280
|
+
if ('animationDuration' in changed)
|
|
4281
|
+
this._config.animationDuration = changed.animationDuration;
|
|
4282
|
+
if ('onClose' in changed)
|
|
4283
|
+
this.onClose = changed.onClose;
|
|
4284
|
+
}
|
|
3586
4285
|
}
|
|
3587
4286
|
|
|
3588
4287
|
const TOAST_COLORS = {
|
|
@@ -3602,17 +4301,21 @@ const TOAST_COLORS = {
|
|
|
3602
4301
|
* ```
|
|
3603
4302
|
*/
|
|
3604
4303
|
class Toast extends Container {
|
|
4304
|
+
__uiComponent = true;
|
|
3605
4305
|
_bg;
|
|
4306
|
+
_customBg;
|
|
3606
4307
|
_text;
|
|
3607
4308
|
_config;
|
|
3608
|
-
|
|
4309
|
+
_dismissPending = false;
|
|
3609
4310
|
constructor(config = {}) {
|
|
3610
4311
|
super();
|
|
3611
4312
|
this._config = {
|
|
3612
4313
|
duration: config.duration ?? 3000,
|
|
3613
4314
|
bottomOffset: config.bottomOffset ?? 60,
|
|
3614
4315
|
};
|
|
3615
|
-
|
|
4316
|
+
const customBg = resolveView(config.backgroundView);
|
|
4317
|
+
this._customBg = !!customBg;
|
|
4318
|
+
this._bg = customBg ?? new Graphics();
|
|
3616
4319
|
this.addChild(this._bg);
|
|
3617
4320
|
this._text = new Text({
|
|
3618
4321
|
text: '',
|
|
@@ -3630,18 +4333,27 @@ class Toast extends Container {
|
|
|
3630
4333
|
* Show a toast message.
|
|
3631
4334
|
*/
|
|
3632
4335
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
4336
|
+
// Cancel any pending dismiss
|
|
4337
|
+
Tween.killTweensOf(this);
|
|
4338
|
+
this._dismissPending = false;
|
|
3636
4339
|
this._text.text = message;
|
|
3637
4340
|
const padding = 20;
|
|
3638
4341
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3639
4342
|
const height = 44;
|
|
3640
4343
|
const radius = 8;
|
|
3641
4344
|
// Draw the background
|
|
3642
|
-
this.
|
|
3643
|
-
|
|
3644
|
-
|
|
4345
|
+
if (this._customBg) {
|
|
4346
|
+
this._bg.width = width;
|
|
4347
|
+
this._bg.height = height;
|
|
4348
|
+
this._bg.x = -width / 2;
|
|
4349
|
+
this._bg.y = -height / 2;
|
|
4350
|
+
}
|
|
4351
|
+
else {
|
|
4352
|
+
const g = this._bg;
|
|
4353
|
+
g.clear();
|
|
4354
|
+
g.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
4355
|
+
g.fill(TOAST_COLORS[type]);
|
|
4356
|
+
}
|
|
3645
4357
|
// Position
|
|
3646
4358
|
if (viewWidth && viewHeight) {
|
|
3647
4359
|
this.x = viewWidth / 2;
|
|
@@ -3652,9 +4364,12 @@ class Toast extends Container {
|
|
|
3652
4364
|
this.y += 20;
|
|
3653
4365
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3654
4366
|
if (this._config.duration > 0) {
|
|
3655
|
-
this.
|
|
3656
|
-
|
|
3657
|
-
|
|
4367
|
+
this._dismissPending = true;
|
|
4368
|
+
await Tween.delay(this._config.duration);
|
|
4369
|
+
if (this._dismissPending) {
|
|
4370
|
+
this._dismissPending = false;
|
|
4371
|
+
await this.dismiss();
|
|
4372
|
+
}
|
|
3658
4373
|
}
|
|
3659
4374
|
}
|
|
3660
4375
|
/**
|
|
@@ -3663,57 +4378,36 @@ class Toast extends Container {
|
|
|
3663
4378
|
async dismiss() {
|
|
3664
4379
|
if (!this.visible)
|
|
3665
4380
|
return;
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
this._dismissTimeout = null;
|
|
3669
|
-
}
|
|
4381
|
+
this._dismissPending = false;
|
|
4382
|
+
Tween.killTweensOf(this);
|
|
3670
4383
|
await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
|
|
3671
4384
|
this.visible = false;
|
|
3672
4385
|
}
|
|
4386
|
+
/** React reconciler update hook */
|
|
4387
|
+
updateConfig(changed) {
|
|
4388
|
+
if ('duration' in changed)
|
|
4389
|
+
this._config.duration = changed.duration;
|
|
4390
|
+
if ('bottomOffset' in changed)
|
|
4391
|
+
this._config.bottomOffset = changed.bottomOffset;
|
|
4392
|
+
}
|
|
4393
|
+
destroy(options) {
|
|
4394
|
+
this._dismissPending = false;
|
|
4395
|
+
Tween.killTweensOf(this);
|
|
4396
|
+
super.destroy(options);
|
|
4397
|
+
}
|
|
3673
4398
|
}
|
|
3674
4399
|
|
|
3675
4400
|
// ─── 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) {
|
|
4401
|
+
function directionToFlex(direction) {
|
|
3688
4402
|
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
|
-
};
|
|
4403
|
+
case 'horizontal': return { direction: 'row', wrap: false };
|
|
4404
|
+
case 'vertical': return { direction: 'column', wrap: false };
|
|
4405
|
+
case 'grid': return { direction: 'row', wrap: true };
|
|
4406
|
+
case 'wrap': return { direction: 'row', wrap: true };
|
|
3701
4407
|
}
|
|
3702
4408
|
}
|
|
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
4409
|
/**
|
|
3716
|
-
* Responsive layout container powered by
|
|
4410
|
+
* Responsive layout container powered by a lightweight built-in flex layout solver.
|
|
3717
4411
|
*
|
|
3718
4412
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3719
4413
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -3740,6 +4434,7 @@ function buildLayoutStyles(config) {
|
|
|
3740
4434
|
* ```
|
|
3741
4435
|
*/
|
|
3742
4436
|
class Layout extends Container {
|
|
4437
|
+
__uiComponent = true;
|
|
3743
4438
|
_layoutConfig;
|
|
3744
4439
|
_padding;
|
|
3745
4440
|
_anchor;
|
|
@@ -3748,6 +4443,7 @@ class Layout extends Container {
|
|
|
3748
4443
|
_items = [];
|
|
3749
4444
|
_viewportWidth = 0;
|
|
3750
4445
|
_viewportHeight = 0;
|
|
4446
|
+
_flex;
|
|
3751
4447
|
constructor(config = {}) {
|
|
3752
4448
|
super();
|
|
3753
4449
|
this._layoutConfig = {
|
|
@@ -3757,7 +4453,7 @@ class Layout extends Container {
|
|
|
3757
4453
|
autoLayout: config.autoLayout ?? true,
|
|
3758
4454
|
columns: config.columns ?? 2,
|
|
3759
4455
|
};
|
|
3760
|
-
this._padding =
|
|
4456
|
+
this._padding = config.padding ?? 0;
|
|
3761
4457
|
this._anchor = config.anchor ?? 'top-left';
|
|
3762
4458
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3763
4459
|
this._breakpoints = config.breakpoints
|
|
@@ -3765,14 +4461,18 @@ class Layout extends Container {
|
|
|
3765
4461
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
3766
4462
|
.sort((a, b) => a[0] - b[0])
|
|
3767
4463
|
: [];
|
|
4464
|
+
// Create internal FlexContainer
|
|
4465
|
+
this._flex = new FlexContainer();
|
|
4466
|
+
super.addChild(this._flex);
|
|
3768
4467
|
this.applyLayoutStyles();
|
|
3769
4468
|
}
|
|
3770
4469
|
/** Add an item to the layout */
|
|
3771
4470
|
addItem(child) {
|
|
3772
4471
|
this._items.push(child);
|
|
3773
|
-
this.
|
|
3774
|
-
|
|
3775
|
-
|
|
4472
|
+
const flexConfig = this.buildFlexItemConfig(child);
|
|
4473
|
+
this._flex.addFlexChild(child, flexConfig);
|
|
4474
|
+
if (this._layoutConfig.autoLayout) {
|
|
4475
|
+
this.applyLayoutStyles();
|
|
3776
4476
|
}
|
|
3777
4477
|
return this;
|
|
3778
4478
|
}
|
|
@@ -3781,15 +4481,13 @@ class Layout extends Container {
|
|
|
3781
4481
|
const idx = this._items.indexOf(child);
|
|
3782
4482
|
if (idx !== -1) {
|
|
3783
4483
|
this._items.splice(idx, 1);
|
|
3784
|
-
this.
|
|
4484
|
+
this._flex.removeFlexChild(child);
|
|
3785
4485
|
}
|
|
3786
4486
|
return this;
|
|
3787
4487
|
}
|
|
3788
4488
|
/** Remove all items */
|
|
3789
4489
|
clearItems() {
|
|
3790
|
-
|
|
3791
|
-
this.removeChild(item);
|
|
3792
|
-
}
|
|
4490
|
+
this._flex.clearFlexChildren();
|
|
3793
4491
|
this._items.length = 0;
|
|
3794
4492
|
return this;
|
|
3795
4493
|
}
|
|
@@ -3812,43 +4510,58 @@ class Layout extends Container {
|
|
|
3812
4510
|
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3813
4511
|
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3814
4512
|
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
3815
|
-
effective.
|
|
3816
|
-
const padding = effective.padding !== undefined
|
|
3817
|
-
? normalizePadding(effective.padding)
|
|
3818
|
-
: this._padding;
|
|
4513
|
+
const padding = effective.padding ?? this._padding;
|
|
3819
4514
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
3820
|
-
const
|
|
3821
|
-
this.
|
|
4515
|
+
const { direction: flexDir, wrap } = directionToFlex(direction);
|
|
4516
|
+
this._flex.setDirection(flexDir);
|
|
4517
|
+
this._flex.setJustifyContent('start');
|
|
4518
|
+
this._flex.setAlignItems(alignment);
|
|
4519
|
+
this._flex.setGap(gap);
|
|
4520
|
+
this._flex.setPadding(padding);
|
|
4521
|
+
// Wrap and maxWidth
|
|
4522
|
+
if (wrap) {
|
|
4523
|
+
this._flex._config.flexWrap = true;
|
|
4524
|
+
if (direction === 'grid' && maxWidth < Infinity) {
|
|
4525
|
+
this._flex._maxWidth = maxWidth;
|
|
4526
|
+
}
|
|
4527
|
+
if (maxWidth < Infinity) {
|
|
4528
|
+
this._flex._maxWidth = maxWidth;
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
else {
|
|
4532
|
+
this._flex._config.flexWrap = false;
|
|
4533
|
+
}
|
|
4534
|
+
// Update grid child widths
|
|
3822
4535
|
if (direction === 'grid') {
|
|
3823
4536
|
for (const item of this._items) {
|
|
3824
|
-
this.
|
|
4537
|
+
const flexConfig = this.buildFlexItemConfig(item);
|
|
4538
|
+
item._flexConfig = flexConfig;
|
|
3825
4539
|
}
|
|
3826
4540
|
}
|
|
4541
|
+
// Set explicit size if we have viewport dimensions
|
|
4542
|
+
if (this._viewportWidth > 0 && this._viewportHeight > 0) {
|
|
4543
|
+
this._flex.resize(this._viewportWidth, this._viewportHeight);
|
|
4544
|
+
}
|
|
4545
|
+
else {
|
|
4546
|
+
this._flex.updateLayout();
|
|
4547
|
+
}
|
|
3827
4548
|
}
|
|
3828
|
-
|
|
4549
|
+
buildFlexItemConfig(_child) {
|
|
3829
4550
|
const effective = this.resolveConfig();
|
|
4551
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3830
4552
|
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;
|
|
4553
|
+
if (direction === 'grid' && columns > 0) {
|
|
4554
|
+
// For grid, give each item a proportional width
|
|
4555
|
+
// The actual pixel width will be computed during layout
|
|
4556
|
+
return { flexGrow: 1 };
|
|
3843
4557
|
}
|
|
4558
|
+
return undefined;
|
|
3844
4559
|
}
|
|
3845
4560
|
applyAnchor() {
|
|
3846
4561
|
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
3847
4562
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3848
4563
|
return;
|
|
3849
|
-
const
|
|
3850
|
-
const contentW = bounds.width * this.scale.x;
|
|
3851
|
-
const contentH = bounds.height * this.scale.y;
|
|
4564
|
+
const { width: contentW, height: contentH } = this._flex.getContentSize();
|
|
3852
4565
|
const vw = this._viewportWidth;
|
|
3853
4566
|
const vh = this._viewportHeight;
|
|
3854
4567
|
let anchorX = 0;
|
|
@@ -3871,8 +4584,8 @@ class Layout extends Container {
|
|
|
3871
4584
|
else {
|
|
3872
4585
|
anchorY = (vh - contentH) / 2;
|
|
3873
4586
|
}
|
|
3874
|
-
this.x = anchorX
|
|
3875
|
-
this.y = anchorY
|
|
4587
|
+
this.x = anchorX;
|
|
4588
|
+
this.y = anchorY;
|
|
3876
4589
|
}
|
|
3877
4590
|
resolveConfig() {
|
|
3878
4591
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
@@ -3885,18 +4598,34 @@ class Layout extends Container {
|
|
|
3885
4598
|
}
|
|
3886
4599
|
return {};
|
|
3887
4600
|
}
|
|
4601
|
+
/** React reconciler update hook */
|
|
4602
|
+
updateConfig(changed) {
|
|
4603
|
+
if ('direction' in changed)
|
|
4604
|
+
this._layoutConfig.direction = changed.direction;
|
|
4605
|
+
if ('gap' in changed)
|
|
4606
|
+
this._layoutConfig.gap = changed.gap;
|
|
4607
|
+
if ('alignment' in changed)
|
|
4608
|
+
this._layoutConfig.alignment = changed.alignment;
|
|
4609
|
+
if ('anchor' in changed)
|
|
4610
|
+
this._anchor = changed.anchor;
|
|
4611
|
+
if ('padding' in changed)
|
|
4612
|
+
this._padding = changed.padding;
|
|
4613
|
+
if ('columns' in changed)
|
|
4614
|
+
this._layoutConfig.columns = changed.columns;
|
|
4615
|
+
this.applyLayoutStyles();
|
|
4616
|
+
if (this._viewportWidth > 0)
|
|
4617
|
+
this.applyAnchor();
|
|
4618
|
+
}
|
|
4619
|
+
destroy(options) {
|
|
4620
|
+
this._items.length = 0;
|
|
4621
|
+
super.destroy(options);
|
|
4622
|
+
}
|
|
3888
4623
|
}
|
|
3889
4624
|
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
horizontal: 'horizontal',
|
|
3893
|
-
both: 'bidirectional',
|
|
3894
|
-
};
|
|
4625
|
+
const DECELERATION = 0.95;
|
|
4626
|
+
const MIN_VELOCITY = 0.5;
|
|
3895
4627
|
/**
|
|
3896
|
-
* Scrollable container
|
|
3897
|
-
*
|
|
3898
|
-
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
3899
|
-
* dynamic rendering optimization for off-screen items.
|
|
4628
|
+
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
3900
4629
|
*
|
|
3901
4630
|
* @example
|
|
3902
4631
|
* ```ts
|
|
@@ -3914,53 +4643,349 @@ const DIRECTION_MAP = {
|
|
|
3914
4643
|
* scene.container.addChild(scroll);
|
|
3915
4644
|
* ```
|
|
3916
4645
|
*/
|
|
3917
|
-
class ScrollContainer extends
|
|
4646
|
+
class ScrollContainer extends Container {
|
|
4647
|
+
__uiComponent = true;
|
|
4648
|
+
_viewport;
|
|
4649
|
+
_internalSetup = true;
|
|
4650
|
+
_content;
|
|
4651
|
+
_maskGfx;
|
|
4652
|
+
_bg = null;
|
|
3918
4653
|
_scrollConfig;
|
|
4654
|
+
_items = [];
|
|
4655
|
+
// Scrollbar
|
|
4656
|
+
_scrollbar = null;
|
|
4657
|
+
_scrollbarConfig;
|
|
4658
|
+
// Drag state
|
|
4659
|
+
_dragging = false;
|
|
4660
|
+
_dragStart = { x: 0, y: 0 };
|
|
4661
|
+
_contentStart = { x: 0, y: 0 };
|
|
4662
|
+
_velocity = { x: 0, y: 0 };
|
|
4663
|
+
_lastDragPos = { x: 0, y: 0 };
|
|
4664
|
+
_lastDragTime = 0;
|
|
4665
|
+
_inertiaActive = false;
|
|
4666
|
+
// Bound handlers for cleanup
|
|
4667
|
+
_onTickBound = null;
|
|
4668
|
+
_onWheelBound = null;
|
|
3919
4669
|
constructor(config) {
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
radius: config.borderRadius ?? 0,
|
|
4670
|
+
super();
|
|
4671
|
+
this._viewport = { width: config.width, height: config.height };
|
|
4672
|
+
this._scrollConfig = {
|
|
4673
|
+
direction: config.direction ?? 'vertical',
|
|
3925
4674
|
elementsMargin: config.elementsMargin ?? 0,
|
|
3926
4675
|
padding: config.padding ?? 0,
|
|
3927
|
-
|
|
4676
|
+
borderRadius: config.borderRadius ?? 0,
|
|
3928
4677
|
disableEasing: config.disableEasing ?? false,
|
|
3929
|
-
globalScroll: config.globalScroll ?? true,
|
|
3930
4678
|
};
|
|
4679
|
+
// Background
|
|
3931
4680
|
if (config.backgroundColor !== undefined) {
|
|
3932
|
-
|
|
4681
|
+
this._bg = new Graphics();
|
|
4682
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
4683
|
+
.fill(config.backgroundColor);
|
|
4684
|
+
this.addChild(this._bg);
|
|
4685
|
+
}
|
|
4686
|
+
// Mask
|
|
4687
|
+
this._maskGfx = new Graphics();
|
|
4688
|
+
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
4689
|
+
.fill(0xffffff);
|
|
4690
|
+
this.addChild(this._maskGfx);
|
|
4691
|
+
// Content container
|
|
4692
|
+
this._content = new Container();
|
|
4693
|
+
this._content.mask = this._maskGfx;
|
|
4694
|
+
this.addChild(this._content);
|
|
4695
|
+
// Interaction
|
|
4696
|
+
this.eventMode = 'static';
|
|
4697
|
+
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
4698
|
+
this.on('pointerdown', this._onPointerDown, this);
|
|
4699
|
+
this.on('pointermove', this._onPointerMove, this);
|
|
4700
|
+
this.on('pointerup', this._onPointerUp, this);
|
|
4701
|
+
this.on('pointerupoutside', this._onPointerUp, this);
|
|
4702
|
+
// Mouse wheel
|
|
4703
|
+
this._onWheelBound = this._onWheel.bind(this);
|
|
4704
|
+
// Scrollbar
|
|
4705
|
+
const sbWidth = config.scrollbarWidth ?? 6;
|
|
4706
|
+
const sbPadding = config.scrollbarPadding ?? 4;
|
|
4707
|
+
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
4708
|
+
if (config.scrollbar) {
|
|
4709
|
+
const customThumb = resolveView(config.thumbView);
|
|
4710
|
+
if (customThumb) {
|
|
4711
|
+
this._scrollbar = customThumb;
|
|
4712
|
+
}
|
|
4713
|
+
else {
|
|
4714
|
+
const g = new Graphics();
|
|
4715
|
+
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
4716
|
+
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
4717
|
+
this._scrollbar = g;
|
|
4718
|
+
}
|
|
4719
|
+
this._scrollbar.visible = false;
|
|
4720
|
+
super.addChild(this._scrollbar);
|
|
3933
4721
|
}
|
|
3934
|
-
|
|
3935
|
-
this._scrollConfig = config;
|
|
4722
|
+
this._internalSetup = false;
|
|
3936
4723
|
}
|
|
3937
|
-
/**
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4724
|
+
/**
|
|
4725
|
+
* Override addChild so external children are routed to scroll content.
|
|
4726
|
+
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
4727
|
+
*/
|
|
4728
|
+
addChild(...children) {
|
|
4729
|
+
if (this._internalSetup) {
|
|
4730
|
+
return super.addChild(...children);
|
|
4731
|
+
}
|
|
4732
|
+
for (const child of children) {
|
|
4733
|
+
this.addItem(child);
|
|
4734
|
+
}
|
|
4735
|
+
return children[0];
|
|
4736
|
+
}
|
|
4737
|
+
removeChild(...children) {
|
|
4738
|
+
if (this._internalSetup) {
|
|
4739
|
+
return super.removeChild(...children);
|
|
4740
|
+
}
|
|
4741
|
+
for (const child of children) {
|
|
4742
|
+
const idx = this._items.indexOf(child);
|
|
4743
|
+
if (idx !== -1) {
|
|
4744
|
+
this._items.splice(idx, 1);
|
|
4745
|
+
this._content.removeChild(child);
|
|
3944
4746
|
}
|
|
3945
4747
|
}
|
|
3946
|
-
|
|
4748
|
+
this.layoutItems();
|
|
4749
|
+
return children[0];
|
|
4750
|
+
}
|
|
4751
|
+
/** React reconciler update hook */
|
|
4752
|
+
updateConfig(changed) {
|
|
4753
|
+
if ('width' in changed || 'height' in changed) {
|
|
4754
|
+
this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
4758
|
+
enableWheel(canvas) {
|
|
4759
|
+
if (this._onWheelBound) {
|
|
4760
|
+
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
/** Set scrollable content. Replaces any existing items. */
|
|
4764
|
+
setContent(content) {
|
|
4765
|
+
this.clearItems();
|
|
3947
4766
|
const children = [...content.children];
|
|
3948
|
-
|
|
3949
|
-
this.
|
|
4767
|
+
for (const child of children) {
|
|
4768
|
+
this.addItem(child);
|
|
3950
4769
|
}
|
|
3951
4770
|
}
|
|
3952
4771
|
/** Add a single item */
|
|
3953
|
-
addItem(
|
|
3954
|
-
this.
|
|
3955
|
-
|
|
4772
|
+
addItem(child) {
|
|
4773
|
+
this._items.push(child);
|
|
4774
|
+
this._content.addChild(child);
|
|
4775
|
+
this.layoutItems();
|
|
4776
|
+
return this;
|
|
4777
|
+
}
|
|
4778
|
+
/** Remove all items */
|
|
4779
|
+
clearItems() {
|
|
4780
|
+
for (const item of this._items) {
|
|
4781
|
+
this._content.removeChild(item);
|
|
4782
|
+
}
|
|
4783
|
+
this._items.length = 0;
|
|
4784
|
+
}
|
|
4785
|
+
/** Get items */
|
|
4786
|
+
get items() {
|
|
4787
|
+
return this._items;
|
|
3956
4788
|
}
|
|
3957
|
-
/** Scroll to make a specific item
|
|
4789
|
+
/** Scroll to make a specific item index visible */
|
|
3958
4790
|
scrollToItem(index) {
|
|
3959
|
-
this.
|
|
4791
|
+
if (index < 0 || index >= this._items.length)
|
|
4792
|
+
return;
|
|
4793
|
+
const item = this._items[index];
|
|
4794
|
+
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
4795
|
+
if (isVert) {
|
|
4796
|
+
this._content.y = -item.y + this._scrollConfig.padding;
|
|
4797
|
+
}
|
|
4798
|
+
else {
|
|
4799
|
+
this._content.x = -item.x + this._scrollConfig.padding;
|
|
4800
|
+
}
|
|
4801
|
+
this.clampScroll();
|
|
3960
4802
|
}
|
|
3961
4803
|
/** Current scroll position */
|
|
3962
4804
|
get scrollPosition() {
|
|
3963
|
-
return { x: this.
|
|
4805
|
+
return { x: this._content.x, y: this._content.y };
|
|
4806
|
+
}
|
|
4807
|
+
/** Resize the scroll viewport */
|
|
4808
|
+
setViewportSize(width, height) {
|
|
4809
|
+
this._viewport.width = width;
|
|
4810
|
+
this._viewport.height = height;
|
|
4811
|
+
this._maskGfx.clear();
|
|
4812
|
+
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
4813
|
+
if (this._bg) {
|
|
4814
|
+
this._bg.clear();
|
|
4815
|
+
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
4816
|
+
.fill(0xffffff); // color will be overridden if needed
|
|
4817
|
+
}
|
|
4818
|
+
this.clampScroll();
|
|
4819
|
+
}
|
|
4820
|
+
// ─── Layout ──────────────────────────────────────────
|
|
4821
|
+
layoutItems() {
|
|
4822
|
+
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
4823
|
+
const isVert = direction !== 'horizontal';
|
|
4824
|
+
let pos = padding;
|
|
4825
|
+
for (const item of this._items) {
|
|
4826
|
+
if (isVert) {
|
|
4827
|
+
item.x = padding;
|
|
4828
|
+
item.y = pos;
|
|
4829
|
+
pos += item.height + elementsMargin;
|
|
4830
|
+
}
|
|
4831
|
+
else {
|
|
4832
|
+
item.x = pos;
|
|
4833
|
+
item.y = padding;
|
|
4834
|
+
pos += item.width + elementsMargin;
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
// ─── Drag handling ───────────────────────────────────
|
|
4839
|
+
_onPointerDown(e) {
|
|
4840
|
+
this._dragging = true;
|
|
4841
|
+
this._inertiaActive = false;
|
|
4842
|
+
this._dragStart.x = e.globalX;
|
|
4843
|
+
this._dragStart.y = e.globalY;
|
|
4844
|
+
this._contentStart.x = this._content.x;
|
|
4845
|
+
this._contentStart.y = this._content.y;
|
|
4846
|
+
this._lastDragPos.x = e.globalX;
|
|
4847
|
+
this._lastDragPos.y = e.globalY;
|
|
4848
|
+
this._lastDragTime = Date.now();
|
|
4849
|
+
this._velocity.x = 0;
|
|
4850
|
+
this._velocity.y = 0;
|
|
4851
|
+
this.stopInertia();
|
|
4852
|
+
}
|
|
4853
|
+
_onPointerMove(e) {
|
|
4854
|
+
if (!this._dragging)
|
|
4855
|
+
return;
|
|
4856
|
+
const dx = e.globalX - this._dragStart.x;
|
|
4857
|
+
const dy = e.globalY - this._dragStart.y;
|
|
4858
|
+
const { direction } = this._scrollConfig;
|
|
4859
|
+
if (direction !== 'horizontal') {
|
|
4860
|
+
this._content.y = this._contentStart.y + dy;
|
|
4861
|
+
}
|
|
4862
|
+
if (direction !== 'vertical') {
|
|
4863
|
+
this._content.x = this._contentStart.x + dx;
|
|
4864
|
+
}
|
|
4865
|
+
// Track velocity
|
|
4866
|
+
const now = Date.now();
|
|
4867
|
+
const dt = now - this._lastDragTime;
|
|
4868
|
+
if (dt > 0) {
|
|
4869
|
+
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
4870
|
+
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
4871
|
+
}
|
|
4872
|
+
this._lastDragPos.x = e.globalX;
|
|
4873
|
+
this._lastDragPos.y = e.globalY;
|
|
4874
|
+
this._lastDragTime = now;
|
|
4875
|
+
this.clampScroll();
|
|
4876
|
+
}
|
|
4877
|
+
_onPointerUp() {
|
|
4878
|
+
if (!this._dragging)
|
|
4879
|
+
return;
|
|
4880
|
+
this._dragging = false;
|
|
4881
|
+
if (!this._scrollConfig.disableEasing &&
|
|
4882
|
+
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
4883
|
+
this.startInertia();
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
// ─── Inertia ─────────────────────────────────────────
|
|
4887
|
+
startInertia() {
|
|
4888
|
+
this._inertiaActive = true;
|
|
4889
|
+
this._onTickBound = this._inertiaTick.bind(this);
|
|
4890
|
+
Ticker.shared.add(this._onTickBound);
|
|
4891
|
+
}
|
|
4892
|
+
stopInertia() {
|
|
4893
|
+
if (this._onTickBound && this._inertiaActive) {
|
|
4894
|
+
Ticker.shared.remove(this._onTickBound);
|
|
4895
|
+
this._inertiaActive = false;
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
_inertiaTick() {
|
|
4899
|
+
const { direction } = this._scrollConfig;
|
|
4900
|
+
if (direction !== 'horizontal') {
|
|
4901
|
+
this._content.y += this._velocity.y;
|
|
4902
|
+
this._velocity.y *= DECELERATION;
|
|
4903
|
+
}
|
|
4904
|
+
if (direction !== 'vertical') {
|
|
4905
|
+
this._content.x += this._velocity.x;
|
|
4906
|
+
this._velocity.x *= DECELERATION;
|
|
4907
|
+
}
|
|
4908
|
+
this.clampScroll();
|
|
4909
|
+
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
4910
|
+
this.stopInertia();
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
// ─── Mouse wheel ─────────────────────────────────────
|
|
4914
|
+
_onWheel(e) {
|
|
4915
|
+
const { direction } = this._scrollConfig;
|
|
4916
|
+
e.preventDefault();
|
|
4917
|
+
if (direction !== 'horizontal') {
|
|
4918
|
+
this._content.y -= e.deltaY;
|
|
4919
|
+
}
|
|
4920
|
+
if (direction !== 'vertical') {
|
|
4921
|
+
this._content.x -= e.deltaX;
|
|
4922
|
+
}
|
|
4923
|
+
this.clampScroll();
|
|
4924
|
+
}
|
|
4925
|
+
// ─── Scroll bounds ───────────────────────────────────
|
|
4926
|
+
clampScroll() {
|
|
4927
|
+
const { direction } = this._scrollConfig;
|
|
4928
|
+
const bounds = this._content.getLocalBounds();
|
|
4929
|
+
if (direction !== 'horizontal') {
|
|
4930
|
+
const contentHeight = bounds.height + bounds.y;
|
|
4931
|
+
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
4932
|
+
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
4933
|
+
}
|
|
4934
|
+
if (direction !== 'vertical') {
|
|
4935
|
+
const contentWidth = bounds.width + bounds.x;
|
|
4936
|
+
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
4937
|
+
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
4938
|
+
}
|
|
4939
|
+
this.updateScrollbar();
|
|
4940
|
+
}
|
|
4941
|
+
updateScrollbar() {
|
|
4942
|
+
if (!this._scrollbar)
|
|
4943
|
+
return;
|
|
4944
|
+
const { direction } = this._scrollConfig;
|
|
4945
|
+
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
4946
|
+
const bounds = this._content.getLocalBounds();
|
|
4947
|
+
const isVert = direction !== 'horizontal';
|
|
4948
|
+
if (isVert) {
|
|
4949
|
+
const contentH = bounds.height + bounds.y;
|
|
4950
|
+
if (contentH <= this._viewport.height) {
|
|
4951
|
+
this._scrollbar.visible = false;
|
|
4952
|
+
return;
|
|
4953
|
+
}
|
|
4954
|
+
this._scrollbar.visible = true;
|
|
4955
|
+
const ratio = this._viewport.height / contentH;
|
|
4956
|
+
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
4957
|
+
const scrollRange = this._viewport.height - thumbH;
|
|
4958
|
+
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
4959
|
+
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
4960
|
+
this._scrollbar.y = scrollProgress * scrollRange;
|
|
4961
|
+
this._scrollbar.height = thumbH;
|
|
4962
|
+
this._scrollbar.width = sbW;
|
|
4963
|
+
}
|
|
4964
|
+
else {
|
|
4965
|
+
const contentW = bounds.width + bounds.x;
|
|
4966
|
+
if (contentW <= this._viewport.width) {
|
|
4967
|
+
this._scrollbar.visible = false;
|
|
4968
|
+
return;
|
|
4969
|
+
}
|
|
4970
|
+
this._scrollbar.visible = true;
|
|
4971
|
+
const ratio = this._viewport.width / contentW;
|
|
4972
|
+
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
4973
|
+
const scrollRange = this._viewport.width - thumbW;
|
|
4974
|
+
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
4975
|
+
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
4976
|
+
this._scrollbar.x = scrollProgress * scrollRange;
|
|
4977
|
+
this._scrollbar.width = thumbW;
|
|
4978
|
+
this._scrollbar.height = sbW;
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
destroy(options) {
|
|
4982
|
+
this.stopInertia();
|
|
4983
|
+
this.off('pointerdown', this._onPointerDown, this);
|
|
4984
|
+
this.off('pointermove', this._onPointerMove, this);
|
|
4985
|
+
this.off('pointerup', this._onPointerUp, this);
|
|
4986
|
+
this.off('pointerupoutside', this._onPointerUp, this);
|
|
4987
|
+
this._items.length = 0;
|
|
4988
|
+
super.destroy(options);
|
|
3964
4989
|
}
|
|
3965
4990
|
}
|
|
3966
4991
|
|
|
@@ -4195,5 +5220,5 @@ class DevBridge {
|
|
|
4195
5220
|
}
|
|
4196
5221
|
}
|
|
4197
5222
|
|
|
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 };
|
|
5223
|
+
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
5224
|
//# sourceMappingURL=index.esm.js.map
|