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