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