@energy8platform/game-engine 0.3.0 → 0.4.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 +139 -44
- package/dist/core.cjs.js +1 -0
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +1 -0
- package/dist/core.esm.js.map +1 -1
- package/dist/index.cjs.js +317 -789
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +61 -129
- package/dist/index.esm.js +304 -790
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +637 -1106
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +60 -128
- package/dist/ui.esm.js +620 -1107
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +23 -3
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +23 -3
- package/dist/vite.esm.js.map +1 -1
- package/package.json +17 -2
- package/src/core/GameApplication.ts +1 -0
- package/src/index.ts +16 -0
- package/src/ui/BalanceDisplay.ts +0 -3
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +102 -180
- package/src/ui/Modal.ts +6 -5
- package/src/ui/Panel.ts +52 -55
- package/src/ui/ProgressBar.ts +52 -57
- package/src/ui/ScrollContainer.ts +58 -489
- package/src/ui/Toast.ts +19 -13
- package/src/ui/index.ts +13 -0
- package/src/vite/index.ts +23 -3
package/dist/index.cjs.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var pixi_js = require('pixi.js');
|
|
4
4
|
var gameSdk = require('@energy8platform/game-sdk');
|
|
5
|
+
require('@pixi/layout');
|
|
6
|
+
var ui = require('@pixi/ui');
|
|
7
|
+
var components = require('@pixi/layout/components');
|
|
5
8
|
|
|
6
9
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
7
10
|
exports.ScaleMode = void 0;
|
|
@@ -2174,6 +2177,7 @@ class GameApplication extends EventEmitter {
|
|
|
2174
2177
|
async initPixi() {
|
|
2175
2178
|
this.app = new pixi_js.Application();
|
|
2176
2179
|
const pixiOpts = {
|
|
2180
|
+
preference: 'webgl',
|
|
2177
2181
|
background: typeof this.config.loading?.backgroundColor === 'number'
|
|
2178
2182
|
? this.config.loading.backgroundColor
|
|
2179
2183
|
: 0x000000,
|
|
@@ -2881,161 +2885,112 @@ class SpriteAnimation {
|
|
|
2881
2885
|
}
|
|
2882
2886
|
|
|
2883
2887
|
const DEFAULT_COLORS = {
|
|
2884
|
-
|
|
2888
|
+
default: 0xffd700,
|
|
2885
2889
|
hover: 0xffe44d,
|
|
2886
2890
|
pressed: 0xccac00,
|
|
2887
2891
|
disabled: 0x666666,
|
|
2888
2892
|
};
|
|
2893
|
+
function makeGraphicsView(w, h, radius, color) {
|
|
2894
|
+
const g = new pixi_js.Graphics();
|
|
2895
|
+
g.roundRect(0, 0, w, h, radius).fill(color);
|
|
2896
|
+
// Highlight overlay
|
|
2897
|
+
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
2898
|
+
return g;
|
|
2899
|
+
}
|
|
2889
2900
|
/**
|
|
2890
|
-
* Interactive button component
|
|
2901
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
2891
2902
|
*
|
|
2892
|
-
* Supports both texture-based and Graphics-based rendering
|
|
2903
|
+
* Supports both texture-based and Graphics-based rendering with
|
|
2904
|
+
* per-state views, press animation, and text.
|
|
2893
2905
|
*
|
|
2894
2906
|
* @example
|
|
2895
2907
|
* ```ts
|
|
2896
2908
|
* const btn = new Button({
|
|
2897
2909
|
* width: 200, height: 60, borderRadius: 12,
|
|
2898
|
-
* colors: {
|
|
2910
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2911
|
+
* text: 'SPIN',
|
|
2899
2912
|
* });
|
|
2900
2913
|
*
|
|
2901
|
-
* btn.
|
|
2914
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
2902
2915
|
* scene.container.addChild(btn);
|
|
2903
2916
|
* ```
|
|
2904
2917
|
*/
|
|
2905
|
-
class Button extends
|
|
2906
|
-
|
|
2907
|
-
_bg;
|
|
2908
|
-
_sprites = {};
|
|
2909
|
-
_config;
|
|
2910
|
-
/** Called when the button is tapped/clicked */
|
|
2911
|
-
onTap;
|
|
2912
|
-
/** Called when the button state changes */
|
|
2913
|
-
onStateChange;
|
|
2918
|
+
class Button extends ui.FancyButton {
|
|
2919
|
+
_buttonConfig;
|
|
2914
2920
|
constructor(config = {}) {
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
animationDuration: 100,
|
|
2921
|
+
const resolvedConfig = {
|
|
2922
|
+
width: config.width ?? 200,
|
|
2923
|
+
height: config.height ?? 60,
|
|
2924
|
+
borderRadius: config.borderRadius ?? 8,
|
|
2925
|
+
pressScale: config.pressScale ?? 0.95,
|
|
2926
|
+
animationDuration: config.animationDuration ?? 100,
|
|
2922
2927
|
...config,
|
|
2923
2928
|
};
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2929
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
2930
|
+
const { width, height, borderRadius } = resolvedConfig;
|
|
2931
|
+
// Build FancyButton options
|
|
2932
|
+
const options = {
|
|
2933
|
+
anchor: 0.5,
|
|
2934
|
+
animations: {
|
|
2935
|
+
hover: {
|
|
2936
|
+
props: { scale: { x: 1.03, y: 1.03 } },
|
|
2937
|
+
duration: resolvedConfig.animationDuration,
|
|
2938
|
+
},
|
|
2939
|
+
pressed: {
|
|
2940
|
+
props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
|
|
2941
|
+
duration: resolvedConfig.animationDuration,
|
|
2942
|
+
},
|
|
2943
|
+
},
|
|
2944
|
+
};
|
|
2945
|
+
// Texture-based views
|
|
2928
2946
|
if (config.textures) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2947
|
+
if (config.textures.default)
|
|
2948
|
+
options.defaultView = config.textures.default;
|
|
2949
|
+
if (config.textures.hover)
|
|
2950
|
+
options.hoverView = config.textures.hover;
|
|
2951
|
+
if (config.textures.pressed)
|
|
2952
|
+
options.pressedView = config.textures.pressed;
|
|
2953
|
+
if (config.textures.disabled)
|
|
2954
|
+
options.disabledView = config.textures.disabled;
|
|
2937
2955
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
this.
|
|
2956
|
+
else {
|
|
2957
|
+
// Graphics-based views
|
|
2958
|
+
options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
|
|
2959
|
+
options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
|
|
2960
|
+
options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
|
|
2961
|
+
options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
|
|
2962
|
+
}
|
|
2963
|
+
// Text
|
|
2964
|
+
if (config.text) {
|
|
2965
|
+
options.text = config.text;
|
|
2966
|
+
}
|
|
2967
|
+
super(options);
|
|
2968
|
+
this._buttonConfig = resolvedConfig;
|
|
2951
2969
|
if (config.disabled) {
|
|
2952
|
-
this.
|
|
2970
|
+
this.enabled = false;
|
|
2953
2971
|
}
|
|
2954
2972
|
}
|
|
2955
|
-
/** Current button state */
|
|
2956
|
-
get state() {
|
|
2957
|
-
return this._state;
|
|
2958
|
-
}
|
|
2959
2973
|
/** Enable the button */
|
|
2960
2974
|
enable() {
|
|
2961
|
-
|
|
2962
|
-
this.setState('normal');
|
|
2963
|
-
this.eventMode = 'static';
|
|
2964
|
-
this.cursor = 'pointer';
|
|
2965
|
-
}
|
|
2975
|
+
this.enabled = true;
|
|
2966
2976
|
}
|
|
2967
2977
|
/** Disable the button */
|
|
2968
2978
|
disable() {
|
|
2969
|
-
this.
|
|
2970
|
-
this.eventMode = 'none';
|
|
2971
|
-
this.cursor = 'default';
|
|
2979
|
+
this.enabled = false;
|
|
2972
2980
|
}
|
|
2973
2981
|
/** Whether the button is disabled */
|
|
2974
2982
|
get disabled() {
|
|
2975
|
-
return this.
|
|
2983
|
+
return !this.enabled;
|
|
2976
2984
|
}
|
|
2977
|
-
setState(state) {
|
|
2978
|
-
if (this._state === state)
|
|
2979
|
-
return;
|
|
2980
|
-
this._state = state;
|
|
2981
|
-
this.render();
|
|
2982
|
-
this.onStateChange?.(state);
|
|
2983
|
-
}
|
|
2984
|
-
render() {
|
|
2985
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
2986
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
2987
|
-
// Update Graphics
|
|
2988
|
-
this._bg.clear();
|
|
2989
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
2990
|
-
// Add highlight for normal/hover
|
|
2991
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
2992
|
-
this._bg
|
|
2993
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
2994
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
2995
|
-
}
|
|
2996
|
-
// Update sprite visibility
|
|
2997
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
2998
|
-
if (sprite)
|
|
2999
|
-
sprite.visible = state === this._state;
|
|
3000
|
-
}
|
|
3001
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
3002
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
3003
|
-
this._sprites.normal.visible = true;
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
onPointerOver = () => {
|
|
3007
|
-
if (this._state === 'disabled')
|
|
3008
|
-
return;
|
|
3009
|
-
this.setState('hover');
|
|
3010
|
-
};
|
|
3011
|
-
onPointerOut = () => {
|
|
3012
|
-
if (this._state === 'disabled')
|
|
3013
|
-
return;
|
|
3014
|
-
this.setState('normal');
|
|
3015
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
3016
|
-
};
|
|
3017
|
-
onPointerDown = () => {
|
|
3018
|
-
if (this._state === 'disabled')
|
|
3019
|
-
return;
|
|
3020
|
-
this.setState('pressed');
|
|
3021
|
-
const s = this._config.pressScale;
|
|
3022
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
3023
|
-
};
|
|
3024
|
-
onPointerUp = () => {
|
|
3025
|
-
if (this._state === 'disabled')
|
|
3026
|
-
return;
|
|
3027
|
-
this.setState('hover');
|
|
3028
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
3029
|
-
};
|
|
3030
|
-
onPointerTap = () => {
|
|
3031
|
-
if (this._state === 'disabled')
|
|
3032
|
-
return;
|
|
3033
|
-
this.onTap?.();
|
|
3034
|
-
};
|
|
3035
2985
|
}
|
|
3036
2986
|
|
|
2987
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
2988
|
+
return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
2989
|
+
}
|
|
3037
2990
|
/**
|
|
3038
|
-
* Horizontal progress bar
|
|
2991
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
2992
|
+
*
|
|
2993
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
3039
2994
|
*
|
|
3040
2995
|
* @example
|
|
3041
2996
|
* ```ts
|
|
@@ -3045,33 +3000,48 @@ class Button extends pixi_js.Container {
|
|
|
3045
3000
|
* ```
|
|
3046
3001
|
*/
|
|
3047
3002
|
class ProgressBar extends pixi_js.Container {
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
_border;
|
|
3003
|
+
_bar;
|
|
3004
|
+
_borderGfx;
|
|
3051
3005
|
_config;
|
|
3052
3006
|
_progress = 0;
|
|
3053
3007
|
_displayedProgress = 0;
|
|
3054
3008
|
constructor(config = {}) {
|
|
3055
3009
|
super();
|
|
3056
3010
|
this._config = {
|
|
3057
|
-
width: 300,
|
|
3058
|
-
height: 16,
|
|
3059
|
-
borderRadius: 8,
|
|
3060
|
-
fillColor: 0xffd700,
|
|
3061
|
-
trackColor: 0x333333,
|
|
3062
|
-
borderColor: 0x555555,
|
|
3063
|
-
borderWidth: 1,
|
|
3064
|
-
animated: true,
|
|
3065
|
-
animationSpeed: 0.1,
|
|
3066
|
-
|
|
3011
|
+
width: config.width ?? 300,
|
|
3012
|
+
height: config.height ?? 16,
|
|
3013
|
+
borderRadius: config.borderRadius ?? 8,
|
|
3014
|
+
fillColor: config.fillColor ?? 0xffd700,
|
|
3015
|
+
trackColor: config.trackColor ?? 0x333333,
|
|
3016
|
+
borderColor: config.borderColor ?? 0x555555,
|
|
3017
|
+
borderWidth: config.borderWidth ?? 1,
|
|
3018
|
+
animated: config.animated ?? true,
|
|
3019
|
+
animationSpeed: config.animationSpeed ?? 0.1,
|
|
3020
|
+
};
|
|
3021
|
+
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
3022
|
+
const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
|
|
3023
|
+
const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
|
|
3024
|
+
const options = {
|
|
3025
|
+
bg: bgGraphics,
|
|
3026
|
+
fill: fillGraphics,
|
|
3027
|
+
fillPaddings: {
|
|
3028
|
+
top: borderWidth,
|
|
3029
|
+
right: borderWidth,
|
|
3030
|
+
bottom: borderWidth,
|
|
3031
|
+
left: borderWidth,
|
|
3032
|
+
},
|
|
3033
|
+
progress: 0,
|
|
3067
3034
|
};
|
|
3068
|
-
this.
|
|
3069
|
-
this.
|
|
3070
|
-
|
|
3071
|
-
this.
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3035
|
+
this._bar = new ui.ProgressBar(options);
|
|
3036
|
+
this.addChild(this._bar);
|
|
3037
|
+
// Border overlay
|
|
3038
|
+
this._borderGfx = new pixi_js.Graphics();
|
|
3039
|
+
if (borderColor !== undefined && borderWidth > 0) {
|
|
3040
|
+
this._borderGfx
|
|
3041
|
+
.roundRect(0, 0, width, height, borderRadius)
|
|
3042
|
+
.stroke({ color: borderColor, width: borderWidth });
|
|
3043
|
+
}
|
|
3044
|
+
this.addChild(this._borderGfx);
|
|
3075
3045
|
}
|
|
3076
3046
|
/** Get/set progress (0..1) */
|
|
3077
3047
|
get progress() {
|
|
@@ -3081,13 +3051,13 @@ class ProgressBar extends pixi_js.Container {
|
|
|
3081
3051
|
this._progress = Math.max(0, Math.min(1, value));
|
|
3082
3052
|
if (!this._config.animated) {
|
|
3083
3053
|
this._displayedProgress = this._progress;
|
|
3084
|
-
this.
|
|
3054
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
3085
3055
|
}
|
|
3086
3056
|
}
|
|
3087
3057
|
/**
|
|
3088
3058
|
* Call each frame if animated is true.
|
|
3089
3059
|
*/
|
|
3090
|
-
update(
|
|
3060
|
+
update(_dt) {
|
|
3091
3061
|
if (!this._config.animated)
|
|
3092
3062
|
return;
|
|
3093
3063
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -3096,35 +3066,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
3096
3066
|
}
|
|
3097
3067
|
this._displayedProgress +=
|
|
3098
3068
|
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
3099
|
-
this.
|
|
3100
|
-
}
|
|
3101
|
-
drawTrack() {
|
|
3102
|
-
const { width, height, borderRadius, trackColor } = this._config;
|
|
3103
|
-
this._track.clear();
|
|
3104
|
-
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
3105
|
-
}
|
|
3106
|
-
drawBorder() {
|
|
3107
|
-
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
3108
|
-
this._border.clear();
|
|
3109
|
-
this._border
|
|
3110
|
-
.roundRect(0, 0, width, height, borderRadius)
|
|
3111
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
3112
|
-
}
|
|
3113
|
-
drawFill(progress) {
|
|
3114
|
-
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
3115
|
-
const innerWidth = width - borderWidth * 2;
|
|
3116
|
-
const innerHeight = height - borderWidth * 2;
|
|
3117
|
-
const fillWidth = Math.max(0, innerWidth * progress);
|
|
3118
|
-
this._fill.clear();
|
|
3119
|
-
if (fillWidth > 0) {
|
|
3120
|
-
this._fill.x = borderWidth;
|
|
3121
|
-
this._fill.y = borderWidth;
|
|
3122
|
-
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
3123
|
-
// Highlight
|
|
3124
|
-
this._fill
|
|
3125
|
-
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
3126
|
-
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
3127
|
-
}
|
|
3069
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
3128
3070
|
}
|
|
3129
3071
|
}
|
|
3130
3072
|
|
|
@@ -3220,7 +3162,10 @@ class Label extends pixi_js.Container {
|
|
|
3220
3162
|
}
|
|
3221
3163
|
|
|
3222
3164
|
/**
|
|
3223
|
-
* Background panel
|
|
3165
|
+
* Background panel powered by `@pixi/layout` LayoutContainer.
|
|
3166
|
+
*
|
|
3167
|
+
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
3168
|
+
* Children added to `content` participate in flexbox layout automatically.
|
|
3224
3169
|
*
|
|
3225
3170
|
* @example
|
|
3226
3171
|
* ```ts
|
|
@@ -3235,72 +3180,67 @@ class Label extends pixi_js.Container {
|
|
|
3235
3180
|
* });
|
|
3236
3181
|
* ```
|
|
3237
3182
|
*/
|
|
3238
|
-
class Panel extends
|
|
3239
|
-
|
|
3240
|
-
_content;
|
|
3241
|
-
_config;
|
|
3183
|
+
class Panel extends components.LayoutContainer {
|
|
3184
|
+
_panelConfig;
|
|
3242
3185
|
constructor(config = {}) {
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
backgroundAlpha: 1,
|
|
3186
|
+
const resolvedConfig = {
|
|
3187
|
+
width: config.width ?? 400,
|
|
3188
|
+
height: config.height ?? 300,
|
|
3189
|
+
padding: config.padding ?? 16,
|
|
3190
|
+
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
3249
3191
|
...config,
|
|
3250
3192
|
};
|
|
3251
|
-
//
|
|
3193
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
3194
|
+
let customBackground;
|
|
3252
3195
|
if (config.nineSliceTexture) {
|
|
3253
3196
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
3254
3197
|
? pixi_js.Texture.from(config.nineSliceTexture)
|
|
3255
3198
|
: config.nineSliceTexture;
|
|
3256
3199
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
3257
|
-
|
|
3200
|
+
const nineSlice = new pixi_js.NineSliceSprite({
|
|
3258
3201
|
texture,
|
|
3259
3202
|
leftWidth: left,
|
|
3260
3203
|
topHeight: top,
|
|
3261
3204
|
rightWidth: right,
|
|
3262
3205
|
bottomHeight: bottom,
|
|
3263
3206
|
});
|
|
3264
|
-
|
|
3265
|
-
|
|
3207
|
+
nineSlice.width = resolvedConfig.width;
|
|
3208
|
+
nineSlice.height = resolvedConfig.height;
|
|
3209
|
+
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
3210
|
+
customBackground = nineSlice;
|
|
3211
|
+
}
|
|
3212
|
+
super(customBackground ? { background: customBackground } : undefined);
|
|
3213
|
+
this._panelConfig = resolvedConfig;
|
|
3214
|
+
// Apply layout styles
|
|
3215
|
+
const layoutStyles = {
|
|
3216
|
+
width: resolvedConfig.width,
|
|
3217
|
+
height: resolvedConfig.height,
|
|
3218
|
+
padding: resolvedConfig.padding,
|
|
3219
|
+
flexDirection: 'column',
|
|
3220
|
+
};
|
|
3221
|
+
// Graphics-based background via layout styles
|
|
3222
|
+
if (!config.nineSliceTexture) {
|
|
3223
|
+
layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3224
|
+
layoutStyles.borderRadius = config.borderRadius ?? 0;
|
|
3225
|
+
if (config.borderColor !== undefined && config.borderWidth) {
|
|
3226
|
+
layoutStyles.borderColor = config.borderColor;
|
|
3227
|
+
layoutStyles.borderWidth = config.borderWidth;
|
|
3228
|
+
}
|
|
3266
3229
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
this.
|
|
3230
|
+
this.layout = layoutStyles;
|
|
3231
|
+
if (!config.nineSliceTexture) {
|
|
3232
|
+
this.background.alpha = resolvedConfig.backgroundAlpha;
|
|
3270
3233
|
}
|
|
3271
|
-
this._bg.alpha = this._config.backgroundAlpha;
|
|
3272
|
-
this.addChild(this._bg);
|
|
3273
|
-
// Content container with padding
|
|
3274
|
-
this._content = new pixi_js.Container();
|
|
3275
|
-
this._content.x = this._config.padding;
|
|
3276
|
-
this._content.y = this._config.padding;
|
|
3277
|
-
this.addChild(this._content);
|
|
3278
3234
|
}
|
|
3279
|
-
/**
|
|
3235
|
+
/** Access the content container (children added here participate in layout) */
|
|
3280
3236
|
get content() {
|
|
3281
|
-
return this.
|
|
3237
|
+
return this.overflowContainer;
|
|
3282
3238
|
}
|
|
3283
3239
|
/** Resize the panel */
|
|
3284
3240
|
setSize(width, height) {
|
|
3285
|
-
this.
|
|
3286
|
-
this.
|
|
3287
|
-
|
|
3288
|
-
this.drawGraphicsBg();
|
|
3289
|
-
}
|
|
3290
|
-
else {
|
|
3291
|
-
this._bg.width = width;
|
|
3292
|
-
this._bg.height = height;
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
drawGraphicsBg() {
|
|
3296
|
-
const bg = this._bg;
|
|
3297
|
-
const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
|
|
3298
|
-
bg.clear();
|
|
3299
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
3300
|
-
if (borderColor !== undefined && borderWidth) {
|
|
3301
|
-
bg.roundRect(0, 0, width, height, borderRadius ?? 0)
|
|
3302
|
-
.stroke({ color: borderColor, width: borderWidth });
|
|
3303
|
-
}
|
|
3241
|
+
this._panelConfig.width = width;
|
|
3242
|
+
this._panelConfig.height = height;
|
|
3243
|
+
this._layout?.setStyle({ width, height });
|
|
3304
3244
|
}
|
|
3305
3245
|
}
|
|
3306
3246
|
|
|
@@ -3388,7 +3328,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
3388
3328
|
this.updateDisplay();
|
|
3389
3329
|
}
|
|
3390
3330
|
async animateValue(from, to) {
|
|
3391
|
-
// Cancel any ongoing animation
|
|
3392
3331
|
if (this._animating) {
|
|
3393
3332
|
this._animationCancelled = true;
|
|
3394
3333
|
}
|
|
@@ -3398,7 +3337,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
3398
3337
|
const startTime = Date.now();
|
|
3399
3338
|
return new Promise((resolve) => {
|
|
3400
3339
|
const tick = () => {
|
|
3401
|
-
// If cancelled by a newer animation, stop immediately
|
|
3402
3340
|
if (this._animationCancelled) {
|
|
3403
3341
|
this._animating = false;
|
|
3404
3342
|
resolve();
|
|
@@ -3540,6 +3478,8 @@ class WinDisplay extends pixi_js.Container {
|
|
|
3540
3478
|
* Modal overlay component.
|
|
3541
3479
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3542
3480
|
*
|
|
3481
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
3482
|
+
*
|
|
3543
3483
|
* @example
|
|
3544
3484
|
* ```ts
|
|
3545
3485
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -3558,11 +3498,10 @@ class Modal extends pixi_js.Container {
|
|
|
3558
3498
|
constructor(config = {}) {
|
|
3559
3499
|
super();
|
|
3560
3500
|
this._config = {
|
|
3561
|
-
overlayColor: 0x000000,
|
|
3562
|
-
overlayAlpha: 0.7,
|
|
3563
|
-
closeOnOverlay: true,
|
|
3564
|
-
animationDuration: 300,
|
|
3565
|
-
...config,
|
|
3501
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
3502
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
3503
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
3504
|
+
animationDuration: config.animationDuration ?? 300,
|
|
3566
3505
|
};
|
|
3567
3506
|
// Overlay
|
|
3568
3507
|
this._overlay = new pixi_js.Graphics();
|
|
@@ -3630,6 +3569,8 @@ const TOAST_COLORS = {
|
|
|
3630
3569
|
/**
|
|
3631
3570
|
* Toast notification component for displaying transient messages.
|
|
3632
3571
|
*
|
|
3572
|
+
* Uses `@pixi/layout` LayoutContainer for auto-sized background.
|
|
3573
|
+
*
|
|
3633
3574
|
* @example
|
|
3634
3575
|
* ```ts
|
|
3635
3576
|
* const toast = new Toast();
|
|
@@ -3645,11 +3586,10 @@ class Toast extends pixi_js.Container {
|
|
|
3645
3586
|
constructor(config = {}) {
|
|
3646
3587
|
super();
|
|
3647
3588
|
this._config = {
|
|
3648
|
-
duration: 3000,
|
|
3649
|
-
bottomOffset: 60,
|
|
3650
|
-
...config,
|
|
3589
|
+
duration: config.duration ?? 3000,
|
|
3590
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
3651
3591
|
};
|
|
3652
|
-
this._bg = new
|
|
3592
|
+
this._bg = new components.LayoutContainer();
|
|
3653
3593
|
this.addChild(this._bg);
|
|
3654
3594
|
this._text = new pixi_js.Text({
|
|
3655
3595
|
text: '',
|
|
@@ -3667,7 +3607,6 @@ class Toast extends pixi_js.Container {
|
|
|
3667
3607
|
* Show a toast message.
|
|
3668
3608
|
*/
|
|
3669
3609
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3670
|
-
// Clear previous dismiss
|
|
3671
3610
|
if (this._dismissTimeout) {
|
|
3672
3611
|
clearTimeout(this._dismissTimeout);
|
|
3673
3612
|
}
|
|
@@ -3676,10 +3615,16 @@ class Toast extends pixi_js.Container {
|
|
|
3676
3615
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3677
3616
|
const height = 44;
|
|
3678
3617
|
const radius = 8;
|
|
3679
|
-
|
|
3680
|
-
this._bg.
|
|
3681
|
-
|
|
3682
|
-
|
|
3618
|
+
// Style the background
|
|
3619
|
+
this._bg.layout = {
|
|
3620
|
+
width,
|
|
3621
|
+
height,
|
|
3622
|
+
borderRadius: radius,
|
|
3623
|
+
backgroundColor: TOAST_COLORS[type],
|
|
3624
|
+
};
|
|
3625
|
+
// Center the bg around origin
|
|
3626
|
+
this._bg.x = -width / 2;
|
|
3627
|
+
this._bg.y = -height / 2;
|
|
3683
3628
|
// Position
|
|
3684
3629
|
if (viewWidth && viewHeight) {
|
|
3685
3630
|
this.x = viewWidth / 2;
|
|
@@ -3688,9 +3633,7 @@ class Toast extends pixi_js.Container {
|
|
|
3688
3633
|
this.visible = true;
|
|
3689
3634
|
this.alpha = 0;
|
|
3690
3635
|
this.y += 20;
|
|
3691
|
-
// Animate in
|
|
3692
3636
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3693
|
-
// Auto-dismiss
|
|
3694
3637
|
if (this._config.duration > 0) {
|
|
3695
3638
|
this._dismissTimeout = setTimeout(() => {
|
|
3696
3639
|
this.dismiss();
|
|
@@ -3712,8 +3655,48 @@ class Toast extends pixi_js.Container {
|
|
|
3712
3655
|
}
|
|
3713
3656
|
}
|
|
3714
3657
|
|
|
3658
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
3659
|
+
const ALIGNMENT_MAP = {
|
|
3660
|
+
start: 'flex-start',
|
|
3661
|
+
center: 'center',
|
|
3662
|
+
end: 'flex-end',
|
|
3663
|
+
stretch: 'stretch',
|
|
3664
|
+
};
|
|
3665
|
+
function normalizePadding(padding) {
|
|
3666
|
+
if (typeof padding === 'number')
|
|
3667
|
+
return [padding, padding, padding, padding];
|
|
3668
|
+
return padding;
|
|
3669
|
+
}
|
|
3670
|
+
function directionToFlexStyles(direction, maxWidth) {
|
|
3671
|
+
switch (direction) {
|
|
3672
|
+
case 'horizontal':
|
|
3673
|
+
return { flexDirection: 'row', flexWrap: 'nowrap' };
|
|
3674
|
+
case 'vertical':
|
|
3675
|
+
return { flexDirection: 'column', flexWrap: 'nowrap' };
|
|
3676
|
+
case 'grid':
|
|
3677
|
+
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
3678
|
+
case 'wrap':
|
|
3679
|
+
return {
|
|
3680
|
+
flexDirection: 'row',
|
|
3681
|
+
flexWrap: 'wrap',
|
|
3682
|
+
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function buildLayoutStyles(config) {
|
|
3687
|
+
const [pt, pr, pb, pl] = config.padding;
|
|
3688
|
+
return {
|
|
3689
|
+
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
3690
|
+
gap: config.gap,
|
|
3691
|
+
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
3692
|
+
paddingTop: pt,
|
|
3693
|
+
paddingRight: pr,
|
|
3694
|
+
paddingBottom: pb,
|
|
3695
|
+
paddingLeft: pl,
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3715
3698
|
/**
|
|
3716
|
-
* Responsive layout container
|
|
3699
|
+
* Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
|
|
3717
3700
|
*
|
|
3718
3701
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3719
3702
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -3736,47 +3719,44 @@ class Toast extends pixi_js.Container {
|
|
|
3736
3719
|
* toolbar.addItem(betLabel);
|
|
3737
3720
|
* scene.container.addChild(toolbar);
|
|
3738
3721
|
*
|
|
3739
|
-
* // On resize, update layout position relative to viewport
|
|
3740
3722
|
* toolbar.updateViewport(width, height);
|
|
3741
3723
|
* ```
|
|
3742
3724
|
*/
|
|
3743
3725
|
class Layout extends pixi_js.Container {
|
|
3744
|
-
|
|
3726
|
+
_layoutConfig;
|
|
3745
3727
|
_padding;
|
|
3746
3728
|
_anchor;
|
|
3747
3729
|
_maxWidth;
|
|
3748
3730
|
_breakpoints;
|
|
3749
|
-
_content;
|
|
3750
3731
|
_items = [];
|
|
3751
3732
|
_viewportWidth = 0;
|
|
3752
3733
|
_viewportHeight = 0;
|
|
3753
3734
|
constructor(config = {}) {
|
|
3754
3735
|
super();
|
|
3755
|
-
this.
|
|
3736
|
+
this._layoutConfig = {
|
|
3756
3737
|
direction: config.direction ?? 'vertical',
|
|
3757
3738
|
gap: config.gap ?? 0,
|
|
3758
3739
|
alignment: config.alignment ?? 'start',
|
|
3759
3740
|
autoLayout: config.autoLayout ?? true,
|
|
3760
3741
|
columns: config.columns ?? 2,
|
|
3761
3742
|
};
|
|
3762
|
-
this._padding =
|
|
3743
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
3763
3744
|
this._anchor = config.anchor ?? 'top-left';
|
|
3764
3745
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3765
|
-
// Sort breakpoints by width ascending for correct resolution
|
|
3766
3746
|
this._breakpoints = config.breakpoints
|
|
3767
3747
|
? Object.entries(config.breakpoints)
|
|
3768
3748
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
3769
3749
|
.sort((a, b) => a[0] - b[0])
|
|
3770
3750
|
: [];
|
|
3771
|
-
this.
|
|
3772
|
-
this.addChild(this._content);
|
|
3751
|
+
this.applyLayoutStyles();
|
|
3773
3752
|
}
|
|
3774
3753
|
/** Add an item to the layout */
|
|
3775
3754
|
addItem(child) {
|
|
3776
3755
|
this._items.push(child);
|
|
3777
|
-
this.
|
|
3778
|
-
if (this.
|
|
3779
|
-
this.
|
|
3756
|
+
this.addChild(child);
|
|
3757
|
+
if (this._layoutConfig.direction === 'grid') {
|
|
3758
|
+
this.applyGridChildWidth(child);
|
|
3759
|
+
}
|
|
3780
3760
|
return this;
|
|
3781
3761
|
}
|
|
3782
3762
|
/** Remove an item from the layout */
|
|
@@ -3784,20 +3764,16 @@ class Layout extends pixi_js.Container {
|
|
|
3784
3764
|
const idx = this._items.indexOf(child);
|
|
3785
3765
|
if (idx !== -1) {
|
|
3786
3766
|
this._items.splice(idx, 1);
|
|
3787
|
-
this.
|
|
3788
|
-
if (this._config.autoLayout)
|
|
3789
|
-
this.layout();
|
|
3767
|
+
this.removeChild(child);
|
|
3790
3768
|
}
|
|
3791
3769
|
return this;
|
|
3792
3770
|
}
|
|
3793
3771
|
/** Remove all items */
|
|
3794
3772
|
clearItems() {
|
|
3795
3773
|
for (const item of this._items) {
|
|
3796
|
-
this.
|
|
3774
|
+
this.removeChild(item);
|
|
3797
3775
|
}
|
|
3798
3776
|
this._items.length = 0;
|
|
3799
|
-
if (this._config.autoLayout)
|
|
3800
|
-
this.layout();
|
|
3801
3777
|
return this;
|
|
3802
3778
|
}
|
|
3803
3779
|
/** Get all layout items */
|
|
@@ -3811,129 +3787,49 @@ class Layout extends pixi_js.Container {
|
|
|
3811
3787
|
updateViewport(width, height) {
|
|
3812
3788
|
this._viewportWidth = width;
|
|
3813
3789
|
this._viewportHeight = height;
|
|
3814
|
-
this.
|
|
3790
|
+
this.applyLayoutStyles();
|
|
3791
|
+
this.applyAnchor();
|
|
3815
3792
|
}
|
|
3816
|
-
|
|
3817
|
-
* Recalculate layout positions of all children.
|
|
3818
|
-
*/
|
|
3819
|
-
layout() {
|
|
3820
|
-
if (this._items.length === 0)
|
|
3821
|
-
return;
|
|
3822
|
-
// Resolve effective config (apply breakpoint overrides)
|
|
3793
|
+
applyLayoutStyles() {
|
|
3823
3794
|
const effective = this.resolveConfig();
|
|
3824
|
-
const
|
|
3825
|
-
const
|
|
3826
|
-
const alignment = effective.alignment ?? this.
|
|
3827
|
-
|
|
3795
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3796
|
+
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3797
|
+
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
3798
|
+
effective.columns ?? this._layoutConfig.columns;
|
|
3828
3799
|
const padding = effective.padding !== undefined
|
|
3829
|
-
?
|
|
3800
|
+
? normalizePadding(effective.padding)
|
|
3830
3801
|
: this._padding;
|
|
3831
3802
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
3832
|
-
const
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
case 'vertical':
|
|
3838
|
-
this.layoutLinear('y', 'x', gap, alignment, pt, pl);
|
|
3839
|
-
break;
|
|
3840
|
-
case 'grid':
|
|
3841
|
-
this.layoutGrid(columns, gap, alignment, pl, pt);
|
|
3842
|
-
break;
|
|
3843
|
-
case 'wrap':
|
|
3844
|
-
this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
|
|
3845
|
-
break;
|
|
3846
|
-
}
|
|
3847
|
-
// Apply anchor positioning relative to viewport
|
|
3848
|
-
this.applyAnchor(effective.anchor ?? this._anchor);
|
|
3849
|
-
}
|
|
3850
|
-
// ─── Private layout helpers ────────────────────────────
|
|
3851
|
-
layoutLinear(mainAxis, crossAxis, gap, alignment, mainOffset, crossOffset) {
|
|
3852
|
-
let pos = mainOffset;
|
|
3853
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3854
|
-
const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
|
|
3855
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
3856
|
-
const item = this._items[i];
|
|
3857
|
-
const size = sizes[i];
|
|
3858
|
-
item[mainAxis] = pos;
|
|
3859
|
-
// Cross-axis alignment
|
|
3860
|
-
const itemCross = crossAxis === 'x' ? size.width : size.height;
|
|
3861
|
-
switch (alignment) {
|
|
3862
|
-
case 'start':
|
|
3863
|
-
item[crossAxis] = crossOffset;
|
|
3864
|
-
break;
|
|
3865
|
-
case 'center':
|
|
3866
|
-
item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
|
|
3867
|
-
break;
|
|
3868
|
-
case 'end':
|
|
3869
|
-
item[crossAxis] = crossOffset + maxCross - itemCross;
|
|
3870
|
-
break;
|
|
3871
|
-
case 'stretch':
|
|
3872
|
-
item[crossAxis] = crossOffset;
|
|
3873
|
-
// Note: stretch doesn't resize children — that's up to the item
|
|
3874
|
-
break;
|
|
3875
|
-
}
|
|
3876
|
-
const mainSize = mainAxis === 'x' ? size.width : size.height;
|
|
3877
|
-
pos += mainSize + gap;
|
|
3878
|
-
}
|
|
3879
|
-
}
|
|
3880
|
-
layoutGrid(columns, gap, alignment, offsetX, offsetY) {
|
|
3881
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3882
|
-
const maxItemWidth = Math.max(...sizes.map(s => s.width));
|
|
3883
|
-
const maxItemHeight = Math.max(...sizes.map(s => s.height));
|
|
3884
|
-
const cellW = maxItemWidth + gap;
|
|
3885
|
-
const cellH = maxItemHeight + gap;
|
|
3886
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
3887
|
-
const item = this._items[i];
|
|
3888
|
-
const col = i % columns;
|
|
3889
|
-
const row = Math.floor(i / columns);
|
|
3890
|
-
const size = sizes[i];
|
|
3891
|
-
// X alignment within cell
|
|
3892
|
-
switch (alignment) {
|
|
3893
|
-
case 'center':
|
|
3894
|
-
item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
|
|
3895
|
-
break;
|
|
3896
|
-
case 'end':
|
|
3897
|
-
item.x = offsetX + col * cellW + maxItemWidth - size.width;
|
|
3898
|
-
break;
|
|
3899
|
-
default:
|
|
3900
|
-
item.x = offsetX + col * cellW;
|
|
3901
|
-
}
|
|
3902
|
-
item.y = offsetY + row * cellH;
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
layoutWrap(maxWidth, gap, alignment, offsetX, offsetY) {
|
|
3906
|
-
let x = offsetX;
|
|
3907
|
-
let y = offsetY;
|
|
3908
|
-
let rowHeight = 0;
|
|
3909
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
3910
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
3911
|
-
const item = this._items[i];
|
|
3912
|
-
const size = sizes[i];
|
|
3913
|
-
// Check if item fits in current row
|
|
3914
|
-
if (x + size.width > maxWidth + offsetX && x > offsetX) {
|
|
3915
|
-
// Wrap to next row
|
|
3916
|
-
x = offsetX;
|
|
3917
|
-
y += rowHeight + gap;
|
|
3918
|
-
rowHeight = 0;
|
|
3803
|
+
const styles = buildLayoutStyles({ direction, gap, alignment, padding, maxWidth });
|
|
3804
|
+
this.layout = styles;
|
|
3805
|
+
if (direction === 'grid') {
|
|
3806
|
+
for (const item of this._items) {
|
|
3807
|
+
this.applyGridChildWidth(item);
|
|
3919
3808
|
}
|
|
3920
|
-
item.x = x;
|
|
3921
|
-
item.y = y;
|
|
3922
|
-
x += size.width + gap;
|
|
3923
|
-
rowHeight = Math.max(rowHeight, size.height);
|
|
3924
3809
|
}
|
|
3925
3810
|
}
|
|
3926
|
-
|
|
3811
|
+
applyGridChildWidth(child) {
|
|
3812
|
+
const effective = this.resolveConfig();
|
|
3813
|
+
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
3814
|
+
const pct = `${(100 / columns).toFixed(2)}%`;
|
|
3815
|
+
if (child._layout) {
|
|
3816
|
+
child._layout.setStyle({ width: pct });
|
|
3817
|
+
}
|
|
3818
|
+
else {
|
|
3819
|
+
child.layout = { width: pct };
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
applyAnchor() {
|
|
3823
|
+
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
3927
3824
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3928
3825
|
return;
|
|
3929
|
-
const bounds = this.
|
|
3930
|
-
const contentW = bounds.width;
|
|
3931
|
-
const contentH = bounds.height;
|
|
3826
|
+
const bounds = this.getLocalBounds();
|
|
3827
|
+
const contentW = bounds.width * this.scale.x;
|
|
3828
|
+
const contentH = bounds.height * this.scale.y;
|
|
3932
3829
|
const vw = this._viewportWidth;
|
|
3933
3830
|
const vh = this._viewportHeight;
|
|
3934
3831
|
let anchorX = 0;
|
|
3935
3832
|
let anchorY = 0;
|
|
3936
|
-
// Horizontal
|
|
3937
3833
|
if (anchor.includes('left')) {
|
|
3938
3834
|
anchorX = 0;
|
|
3939
3835
|
}
|
|
@@ -3941,10 +3837,8 @@ class Layout extends pixi_js.Container {
|
|
|
3941
3837
|
anchorX = vw - contentW;
|
|
3942
3838
|
}
|
|
3943
3839
|
else {
|
|
3944
|
-
// center
|
|
3945
3840
|
anchorX = (vw - contentW) / 2;
|
|
3946
3841
|
}
|
|
3947
|
-
// Vertical
|
|
3948
3842
|
if (anchor.startsWith('top')) {
|
|
3949
3843
|
anchorY = 0;
|
|
3950
3844
|
}
|
|
@@ -3952,44 +3846,34 @@ class Layout extends pixi_js.Container {
|
|
|
3952
3846
|
anchorY = vh - contentH;
|
|
3953
3847
|
}
|
|
3954
3848
|
else {
|
|
3955
|
-
// center
|
|
3956
3849
|
anchorY = (vh - contentH) / 2;
|
|
3957
3850
|
}
|
|
3958
|
-
|
|
3959
|
-
this.
|
|
3960
|
-
this.y = anchorY - bounds.y;
|
|
3851
|
+
this.x = anchorX - bounds.x * this.scale.x;
|
|
3852
|
+
this.y = anchorY - bounds.y * this.scale.y;
|
|
3961
3853
|
}
|
|
3962
3854
|
resolveConfig() {
|
|
3963
3855
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
3964
3856
|
return {};
|
|
3965
3857
|
}
|
|
3966
|
-
// Find the largest breakpoint that's ≤ current viewport width
|
|
3967
|
-
let resolved = {};
|
|
3968
3858
|
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
3969
3859
|
if (this._viewportWidth <= maxWidth) {
|
|
3970
|
-
|
|
3971
|
-
break;
|
|
3860
|
+
return overrides;
|
|
3972
3861
|
}
|
|
3973
3862
|
}
|
|
3974
|
-
return
|
|
3975
|
-
}
|
|
3976
|
-
getItemSize(item) {
|
|
3977
|
-
const bounds = item.getBounds();
|
|
3978
|
-
return { width: bounds.width, height: bounds.height };
|
|
3979
|
-
}
|
|
3980
|
-
static normalizePadding(padding) {
|
|
3981
|
-
if (typeof padding === 'number') {
|
|
3982
|
-
return [padding, padding, padding, padding];
|
|
3983
|
-
}
|
|
3984
|
-
return padding;
|
|
3863
|
+
return {};
|
|
3985
3864
|
}
|
|
3986
3865
|
}
|
|
3987
3866
|
|
|
3867
|
+
const DIRECTION_MAP = {
|
|
3868
|
+
vertical: 'vertical',
|
|
3869
|
+
horizontal: 'horizontal',
|
|
3870
|
+
both: 'bidirectional',
|
|
3871
|
+
};
|
|
3988
3872
|
/**
|
|
3989
|
-
* Scrollable container
|
|
3873
|
+
* Scrollable container powered by `@pixi/ui` ScrollBox.
|
|
3990
3874
|
*
|
|
3991
|
-
*
|
|
3992
|
-
*
|
|
3875
|
+
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
3876
|
+
* dynamic rendering optimization for off-screen items.
|
|
3993
3877
|
*
|
|
3994
3878
|
* @example
|
|
3995
3879
|
* ```ts
|
|
@@ -3997,435 +3881,63 @@ class Layout extends pixi_js.Container {
|
|
|
3997
3881
|
* width: 600,
|
|
3998
3882
|
* height: 400,
|
|
3999
3883
|
* direction: 'vertical',
|
|
4000
|
-
*
|
|
4001
|
-
* elasticity: 0.3,
|
|
3884
|
+
* elementsMargin: 8,
|
|
4002
3885
|
* });
|
|
4003
3886
|
*
|
|
4004
|
-
* // Add content taller than 400px
|
|
4005
|
-
* const list = new Container();
|
|
4006
3887
|
* for (let i = 0; i < 50; i++) {
|
|
4007
|
-
*
|
|
4008
|
-
* row.y = i * 40;
|
|
4009
|
-
* list.addChild(row);
|
|
3888
|
+
* scroll.addItem(createRow(i));
|
|
4010
3889
|
* }
|
|
4011
|
-
* scroll.setContent(list);
|
|
4012
3890
|
*
|
|
4013
3891
|
* scene.container.addChild(scroll);
|
|
4014
3892
|
* ```
|
|
4015
3893
|
*/
|
|
4016
|
-
class ScrollContainer extends
|
|
4017
|
-
|
|
4018
|
-
_viewport;
|
|
4019
|
-
_content = null;
|
|
4020
|
-
_mask;
|
|
4021
|
-
_bg;
|
|
4022
|
-
_scrollbarV = null;
|
|
4023
|
-
_scrollbarH = null;
|
|
4024
|
-
_scrollbarFadeTimeout = null;
|
|
4025
|
-
// Scroll state
|
|
4026
|
-
_scrollX = 0;
|
|
4027
|
-
_scrollY = 0;
|
|
4028
|
-
_velocityX = 0;
|
|
4029
|
-
_velocityY = 0;
|
|
4030
|
-
_isDragging = false;
|
|
4031
|
-
_dragStart = { x: 0, y: 0 };
|
|
4032
|
-
_scrollStart = { x: 0, y: 0 };
|
|
4033
|
-
_lastDragPos = { x: 0, y: 0 };
|
|
4034
|
-
_lastDragTime = 0;
|
|
4035
|
-
_isAnimating = false;
|
|
4036
|
-
_animationFrame = null;
|
|
3894
|
+
class ScrollContainer extends ui.ScrollBox {
|
|
3895
|
+
_scrollConfig;
|
|
4037
3896
|
constructor(config) {
|
|
4038
|
-
|
|
4039
|
-
this._config = {
|
|
3897
|
+
const options = {
|
|
4040
3898
|
width: config.width,
|
|
4041
3899
|
height: config.height,
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
snapSize: config.snapSize ?? 0,
|
|
4050
|
-
borderRadius: config.borderRadius ?? 0,
|
|
3900
|
+
type: DIRECTION_MAP[config.direction ?? 'vertical'],
|
|
3901
|
+
radius: config.borderRadius ?? 0,
|
|
3902
|
+
elementsMargin: config.elementsMargin ?? 0,
|
|
3903
|
+
padding: config.padding ?? 0,
|
|
3904
|
+
disableDynamicRendering: config.disableDynamicRendering ?? false,
|
|
3905
|
+
disableEasing: config.disableEasing ?? false,
|
|
3906
|
+
globalScroll: config.globalScroll ?? true,
|
|
4051
3907
|
};
|
|
4052
|
-
// Background
|
|
4053
|
-
this._bg = new pixi_js.Graphics();
|
|
4054
3908
|
if (config.backgroundColor !== undefined) {
|
|
4055
|
-
|
|
4056
|
-
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
3909
|
+
options.background = config.backgroundColor;
|
|
4057
3910
|
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
this._viewport = new pixi_js.Container();
|
|
4061
|
-
this.addChild(this._viewport);
|
|
4062
|
-
// Mask
|
|
4063
|
-
this._mask = new pixi_js.Graphics();
|
|
4064
|
-
this._mask.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
4065
|
-
.fill(0xffffff);
|
|
4066
|
-
this.addChild(this._mask);
|
|
4067
|
-
this._viewport.mask = this._mask;
|
|
4068
|
-
// Scrollbars
|
|
4069
|
-
if (this._config.showScrollbar) {
|
|
4070
|
-
if (this._config.direction !== 'horizontal') {
|
|
4071
|
-
this._scrollbarV = new pixi_js.Graphics();
|
|
4072
|
-
this._scrollbarV.alpha = 0;
|
|
4073
|
-
this.addChild(this._scrollbarV);
|
|
4074
|
-
}
|
|
4075
|
-
if (this._config.direction !== 'vertical') {
|
|
4076
|
-
this._scrollbarH = new pixi_js.Graphics();
|
|
4077
|
-
this._scrollbarH.alpha = 0;
|
|
4078
|
-
this.addChild(this._scrollbarH);
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
// Interaction
|
|
4082
|
-
this.eventMode = 'static';
|
|
4083
|
-
this.cursor = 'grab';
|
|
4084
|
-
this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
4085
|
-
this.on('pointerdown', this.onPointerDown);
|
|
4086
|
-
this.on('pointermove', this.onPointerMove);
|
|
4087
|
-
this.on('pointerup', this.onPointerUp);
|
|
4088
|
-
this.on('pointerupoutside', this.onPointerUp);
|
|
4089
|
-
this.on('wheel', this.onWheel);
|
|
3911
|
+
super(options);
|
|
3912
|
+
this._scrollConfig = config;
|
|
4090
3913
|
}
|
|
4091
3914
|
/** Set scrollable content. Replaces any existing content. */
|
|
4092
3915
|
setContent(content) {
|
|
4093
|
-
|
|
4094
|
-
|
|
3916
|
+
// Remove existing items
|
|
3917
|
+
const existing = this.items;
|
|
3918
|
+
if (existing.length > 0) {
|
|
3919
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
3920
|
+
this.removeItem(i);
|
|
3921
|
+
}
|
|
4095
3922
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
this.applyScroll();
|
|
4101
|
-
}
|
|
4102
|
-
/** Get the content container */
|
|
4103
|
-
get content() {
|
|
4104
|
-
return this._content;
|
|
4105
|
-
}
|
|
4106
|
-
/** Scroll to a specific position (in content coordinates) */
|
|
4107
|
-
scrollTo(x, y, animate = true) {
|
|
4108
|
-
if (!animate) {
|
|
4109
|
-
this._scrollX = x;
|
|
4110
|
-
this._scrollY = y;
|
|
4111
|
-
this.clampScroll();
|
|
4112
|
-
this.applyScroll();
|
|
4113
|
-
return;
|
|
3923
|
+
// Add all children from the content container
|
|
3924
|
+
const children = [...content.children];
|
|
3925
|
+
if (children.length > 0) {
|
|
3926
|
+
this.addItems(children);
|
|
4114
3927
|
}
|
|
4115
|
-
|
|
3928
|
+
}
|
|
3929
|
+
/** Add a single item */
|
|
3930
|
+
addItem(...items) {
|
|
3931
|
+
this.addItems(items);
|
|
3932
|
+
return items[0];
|
|
4116
3933
|
}
|
|
4117
3934
|
/** Scroll to make a specific item/child visible */
|
|
4118
3935
|
scrollToItem(index) {
|
|
4119
|
-
|
|
4120
|
-
const pos = index * this._config.snapSize;
|
|
4121
|
-
if (this._config.direction === 'horizontal') {
|
|
4122
|
-
this.scrollTo(pos, this._scrollY);
|
|
4123
|
-
}
|
|
4124
|
-
else {
|
|
4125
|
-
this.scrollTo(this._scrollX, pos);
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
3936
|
+
this.scrollTo(index);
|
|
4128
3937
|
}
|
|
4129
3938
|
/** Current scroll position */
|
|
4130
3939
|
get scrollPosition() {
|
|
4131
|
-
return { x: this.
|
|
4132
|
-
}
|
|
4133
|
-
/** Resize the scroll viewport */
|
|
4134
|
-
resize(width, height) {
|
|
4135
|
-
this._config.width = width;
|
|
4136
|
-
this._config.height = height;
|
|
4137
|
-
// Redraw mask and background
|
|
4138
|
-
this._mask.clear();
|
|
4139
|
-
this._mask.roundRect(0, 0, width, height, this._config.borderRadius).fill(0xffffff);
|
|
4140
|
-
this._bg.clear();
|
|
4141
|
-
this.hitArea = { contains: (x, y) => x >= 0 && x <= width && y >= 0 && y <= height };
|
|
4142
|
-
this.clampScroll();
|
|
4143
|
-
this.applyScroll();
|
|
4144
|
-
}
|
|
4145
|
-
/** Destroy and clean up */
|
|
4146
|
-
destroy(options) {
|
|
4147
|
-
this.stopAnimation();
|
|
4148
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
4149
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
4150
|
-
}
|
|
4151
|
-
this.off('pointerdown', this.onPointerDown);
|
|
4152
|
-
this.off('pointermove', this.onPointerMove);
|
|
4153
|
-
this.off('pointerup', this.onPointerUp);
|
|
4154
|
-
this.off('pointerupoutside', this.onPointerUp);
|
|
4155
|
-
this.off('wheel', this.onWheel);
|
|
4156
|
-
super.destroy(options);
|
|
4157
|
-
}
|
|
4158
|
-
// ─── Scroll mechanics ─────────────────────────────────
|
|
4159
|
-
get contentWidth() {
|
|
4160
|
-
if (!this._content)
|
|
4161
|
-
return 0;
|
|
4162
|
-
const bounds = this._content.getBounds();
|
|
4163
|
-
return bounds.width;
|
|
4164
|
-
}
|
|
4165
|
-
get contentHeight() {
|
|
4166
|
-
if (!this._content)
|
|
4167
|
-
return 0;
|
|
4168
|
-
const bounds = this._content.getBounds();
|
|
4169
|
-
return bounds.height;
|
|
4170
|
-
}
|
|
4171
|
-
get maxScrollX() {
|
|
4172
|
-
return Math.max(0, this.contentWidth - this._config.width);
|
|
4173
|
-
}
|
|
4174
|
-
get maxScrollY() {
|
|
4175
|
-
return Math.max(0, this.contentHeight - this._config.height);
|
|
4176
|
-
}
|
|
4177
|
-
canScrollX() {
|
|
4178
|
-
return this._config.direction === 'horizontal' || this._config.direction === 'both';
|
|
4179
|
-
}
|
|
4180
|
-
canScrollY() {
|
|
4181
|
-
return this._config.direction === 'vertical' || this._config.direction === 'both';
|
|
4182
|
-
}
|
|
4183
|
-
clampScroll() {
|
|
4184
|
-
if (this.canScrollX()) {
|
|
4185
|
-
this._scrollX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
4186
|
-
}
|
|
4187
|
-
else {
|
|
4188
|
-
this._scrollX = 0;
|
|
4189
|
-
}
|
|
4190
|
-
if (this.canScrollY()) {
|
|
4191
|
-
this._scrollY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
4192
|
-
}
|
|
4193
|
-
else {
|
|
4194
|
-
this._scrollY = 0;
|
|
4195
|
-
}
|
|
4196
|
-
}
|
|
4197
|
-
applyScroll() {
|
|
4198
|
-
if (!this._content)
|
|
4199
|
-
return;
|
|
4200
|
-
this._content.x = -this._scrollX;
|
|
4201
|
-
this._content.y = -this._scrollY;
|
|
4202
|
-
this.updateScrollbars();
|
|
4203
|
-
}
|
|
4204
|
-
// ─── Input handlers ────────────────────────────────────
|
|
4205
|
-
onPointerDown = (e) => {
|
|
4206
|
-
this._isDragging = true;
|
|
4207
|
-
this._isAnimating = false;
|
|
4208
|
-
this.stopAnimation();
|
|
4209
|
-
this.cursor = 'grabbing';
|
|
4210
|
-
const local = e.getLocalPosition(this);
|
|
4211
|
-
this._dragStart = { x: local.x, y: local.y };
|
|
4212
|
-
this._scrollStart = { x: this._scrollX, y: this._scrollY };
|
|
4213
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
4214
|
-
this._lastDragTime = Date.now();
|
|
4215
|
-
this._velocityX = 0;
|
|
4216
|
-
this._velocityY = 0;
|
|
4217
|
-
this.showScrollbars();
|
|
4218
|
-
};
|
|
4219
|
-
onPointerMove = (e) => {
|
|
4220
|
-
if (!this._isDragging)
|
|
4221
|
-
return;
|
|
4222
|
-
const local = e.getLocalPosition(this);
|
|
4223
|
-
const dx = local.x - this._dragStart.x;
|
|
4224
|
-
const dy = local.y - this._dragStart.y;
|
|
4225
|
-
const now = Date.now();
|
|
4226
|
-
const dt = Math.max(1, now - this._lastDragTime);
|
|
4227
|
-
// Calculate velocity for inertia
|
|
4228
|
-
this._velocityX = (local.x - this._lastDragPos.x) / dt * 16; // normalize to ~60fps
|
|
4229
|
-
this._velocityY = (local.y - this._lastDragPos.y) / dt * 16;
|
|
4230
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
4231
|
-
this._lastDragTime = now;
|
|
4232
|
-
// Apply scroll with elasticity for overscroll
|
|
4233
|
-
let newX = this._scrollStart.x - dx;
|
|
4234
|
-
let newY = this._scrollStart.y - dy;
|
|
4235
|
-
const elasticity = this._config.elasticity;
|
|
4236
|
-
if (this.canScrollX()) {
|
|
4237
|
-
if (newX < 0)
|
|
4238
|
-
newX *= elasticity;
|
|
4239
|
-
else if (newX > this.maxScrollX)
|
|
4240
|
-
newX = this.maxScrollX + (newX - this.maxScrollX) * elasticity;
|
|
4241
|
-
this._scrollX = newX;
|
|
4242
|
-
}
|
|
4243
|
-
if (this.canScrollY()) {
|
|
4244
|
-
if (newY < 0)
|
|
4245
|
-
newY *= elasticity;
|
|
4246
|
-
else if (newY > this.maxScrollY)
|
|
4247
|
-
newY = this.maxScrollY + (newY - this.maxScrollY) * elasticity;
|
|
4248
|
-
this._scrollY = newY;
|
|
4249
|
-
}
|
|
4250
|
-
this.applyScroll();
|
|
4251
|
-
};
|
|
4252
|
-
onPointerUp = () => {
|
|
4253
|
-
if (!this._isDragging)
|
|
4254
|
-
return;
|
|
4255
|
-
this._isDragging = false;
|
|
4256
|
-
this.cursor = 'grab';
|
|
4257
|
-
// Start inertia
|
|
4258
|
-
if (Math.abs(this._velocityX) > 0.5 || Math.abs(this._velocityY) > 0.5) {
|
|
4259
|
-
this.startInertia();
|
|
4260
|
-
}
|
|
4261
|
-
else {
|
|
4262
|
-
this.snapAndBounce();
|
|
4263
|
-
}
|
|
4264
|
-
};
|
|
4265
|
-
onWheel = (e) => {
|
|
4266
|
-
e.preventDefault?.();
|
|
4267
|
-
const delta = e.deltaY ?? 0;
|
|
4268
|
-
const deltaX = e.deltaX ?? 0;
|
|
4269
|
-
if (this.canScrollY()) {
|
|
4270
|
-
this._scrollY += delta * 0.5;
|
|
4271
|
-
}
|
|
4272
|
-
if (this.canScrollX()) {
|
|
4273
|
-
this._scrollX += deltaX * 0.5;
|
|
4274
|
-
}
|
|
4275
|
-
this.clampScroll();
|
|
4276
|
-
this.applyScroll();
|
|
4277
|
-
this.showScrollbars();
|
|
4278
|
-
this.scheduleScrollbarFade();
|
|
4279
|
-
};
|
|
4280
|
-
// ─── Inertia & snap ───────────────────────────────────
|
|
4281
|
-
startInertia() {
|
|
4282
|
-
this._isAnimating = true;
|
|
4283
|
-
const tick = () => {
|
|
4284
|
-
if (!this._isAnimating)
|
|
4285
|
-
return;
|
|
4286
|
-
this._velocityX *= this._config.inertia;
|
|
4287
|
-
this._velocityY *= this._config.inertia;
|
|
4288
|
-
if (this.canScrollX())
|
|
4289
|
-
this._scrollX -= this._velocityX;
|
|
4290
|
-
if (this.canScrollY())
|
|
4291
|
-
this._scrollY -= this._velocityY;
|
|
4292
|
-
// Bounce back if overscrolled
|
|
4293
|
-
let bounced = false;
|
|
4294
|
-
if (this.canScrollX()) {
|
|
4295
|
-
if (this._scrollX < 0) {
|
|
4296
|
-
this._scrollX *= 0.8;
|
|
4297
|
-
bounced = true;
|
|
4298
|
-
}
|
|
4299
|
-
else if (this._scrollX > this.maxScrollX) {
|
|
4300
|
-
this._scrollX = this.maxScrollX + (this._scrollX - this.maxScrollX) * 0.8;
|
|
4301
|
-
bounced = true;
|
|
4302
|
-
}
|
|
4303
|
-
}
|
|
4304
|
-
if (this.canScrollY()) {
|
|
4305
|
-
if (this._scrollY < 0) {
|
|
4306
|
-
this._scrollY *= 0.8;
|
|
4307
|
-
bounced = true;
|
|
4308
|
-
}
|
|
4309
|
-
else if (this._scrollY > this.maxScrollY) {
|
|
4310
|
-
this._scrollY = this.maxScrollY + (this._scrollY - this.maxScrollY) * 0.8;
|
|
4311
|
-
bounced = true;
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
this.applyScroll();
|
|
4315
|
-
const speed = Math.abs(this._velocityX) + Math.abs(this._velocityY);
|
|
4316
|
-
if (speed < 0.1 && !bounced) {
|
|
4317
|
-
this._isAnimating = false;
|
|
4318
|
-
this.snapAndBounce();
|
|
4319
|
-
return;
|
|
4320
|
-
}
|
|
4321
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
4322
|
-
};
|
|
4323
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
4324
|
-
}
|
|
4325
|
-
snapAndBounce() {
|
|
4326
|
-
// Clamp first
|
|
4327
|
-
let targetX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
4328
|
-
let targetY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
4329
|
-
// Snap
|
|
4330
|
-
if (this._config.snapSize > 0) {
|
|
4331
|
-
if (this.canScrollY()) {
|
|
4332
|
-
targetY = Math.round(targetY / this._config.snapSize) * this._config.snapSize;
|
|
4333
|
-
targetY = Math.max(0, Math.min(targetY, this.maxScrollY));
|
|
4334
|
-
}
|
|
4335
|
-
if (this.canScrollX()) {
|
|
4336
|
-
targetX = Math.round(targetX / this._config.snapSize) * this._config.snapSize;
|
|
4337
|
-
targetX = Math.max(0, Math.min(targetX, this.maxScrollX));
|
|
4338
|
-
}
|
|
4339
|
-
}
|
|
4340
|
-
if (Math.abs(targetX - this._scrollX) < 0.5 && Math.abs(targetY - this._scrollY) < 0.5) {
|
|
4341
|
-
this._scrollX = targetX;
|
|
4342
|
-
this._scrollY = targetY;
|
|
4343
|
-
this.applyScroll();
|
|
4344
|
-
this.scheduleScrollbarFade();
|
|
4345
|
-
return;
|
|
4346
|
-
}
|
|
4347
|
-
this.animateScrollTo(targetX, targetY);
|
|
4348
|
-
}
|
|
4349
|
-
animateScrollTo(targetX, targetY) {
|
|
4350
|
-
this._isAnimating = true;
|
|
4351
|
-
const startX = this._scrollX;
|
|
4352
|
-
const startY = this._scrollY;
|
|
4353
|
-
const startTime = Date.now();
|
|
4354
|
-
const duration = 300;
|
|
4355
|
-
const tick = () => {
|
|
4356
|
-
if (!this._isAnimating)
|
|
4357
|
-
return;
|
|
4358
|
-
const elapsed = Date.now() - startTime;
|
|
4359
|
-
const t = Math.min(elapsed / duration, 1);
|
|
4360
|
-
// easeOutCubic
|
|
4361
|
-
const eased = 1 - Math.pow(1 - t, 3);
|
|
4362
|
-
this._scrollX = startX + (targetX - startX) * eased;
|
|
4363
|
-
this._scrollY = startY + (targetY - startY) * eased;
|
|
4364
|
-
this.applyScroll();
|
|
4365
|
-
if (t < 1) {
|
|
4366
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
4367
|
-
}
|
|
4368
|
-
else {
|
|
4369
|
-
this._isAnimating = false;
|
|
4370
|
-
this.scheduleScrollbarFade();
|
|
4371
|
-
}
|
|
4372
|
-
};
|
|
4373
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
4374
|
-
}
|
|
4375
|
-
stopAnimation() {
|
|
4376
|
-
this._isAnimating = false;
|
|
4377
|
-
if (this._animationFrame !== null) {
|
|
4378
|
-
cancelAnimationFrame(this._animationFrame);
|
|
4379
|
-
this._animationFrame = null;
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
// ─── Scrollbars ────────────────────────────────────────
|
|
4383
|
-
updateScrollbars() {
|
|
4384
|
-
const { width, height, scrollbarWidth, scrollbarColor, scrollbarAlpha } = this._config;
|
|
4385
|
-
if (this._scrollbarV && this.canScrollY() && this.contentHeight > height) {
|
|
4386
|
-
const ratio = height / this.contentHeight;
|
|
4387
|
-
const barH = Math.max(20, height * ratio);
|
|
4388
|
-
const barY = (this._scrollY / this.maxScrollY) * (height - barH);
|
|
4389
|
-
this._scrollbarV.clear();
|
|
4390
|
-
this._scrollbarV.roundRect(width - scrollbarWidth - 2, Math.max(0, barY), scrollbarWidth, barH, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
4391
|
-
}
|
|
4392
|
-
if (this._scrollbarH && this.canScrollX() && this.contentWidth > width) {
|
|
4393
|
-
const ratio = width / this.contentWidth;
|
|
4394
|
-
const barW = Math.max(20, width * ratio);
|
|
4395
|
-
const barX = (this._scrollX / this.maxScrollX) * (width - barW);
|
|
4396
|
-
this._scrollbarH.clear();
|
|
4397
|
-
this._scrollbarH.roundRect(Math.max(0, barX), height - scrollbarWidth - 2, barW, scrollbarWidth, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
4398
|
-
}
|
|
4399
|
-
}
|
|
4400
|
-
showScrollbars() {
|
|
4401
|
-
if (this._scrollbarV)
|
|
4402
|
-
this._scrollbarV.alpha = 1;
|
|
4403
|
-
if (this._scrollbarH)
|
|
4404
|
-
this._scrollbarH.alpha = 1;
|
|
4405
|
-
}
|
|
4406
|
-
scheduleScrollbarFade() {
|
|
4407
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
4408
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
4409
|
-
}
|
|
4410
|
-
this._scrollbarFadeTimeout = window.setTimeout(() => {
|
|
4411
|
-
this.fadeScrollbars();
|
|
4412
|
-
}, 1000);
|
|
4413
|
-
}
|
|
4414
|
-
fadeScrollbars() {
|
|
4415
|
-
const duration = 300;
|
|
4416
|
-
const startTime = Date.now();
|
|
4417
|
-
const startAlphaV = this._scrollbarV?.alpha ?? 0;
|
|
4418
|
-
const startAlphaH = this._scrollbarH?.alpha ?? 0;
|
|
4419
|
-
const tick = () => {
|
|
4420
|
-
const t = Math.min((Date.now() - startTime) / duration, 1);
|
|
4421
|
-
if (this._scrollbarV)
|
|
4422
|
-
this._scrollbarV.alpha = startAlphaV * (1 - t);
|
|
4423
|
-
if (this._scrollbarH)
|
|
4424
|
-
this._scrollbarH.alpha = startAlphaH * (1 - t);
|
|
4425
|
-
if (t < 1)
|
|
4426
|
-
requestAnimationFrame(tick);
|
|
4427
|
-
};
|
|
4428
|
-
requestAnimationFrame(tick);
|
|
3940
|
+
return { x: this.scrollX, y: this.scrollY };
|
|
4429
3941
|
}
|
|
4430
3942
|
}
|
|
4431
3943
|
|
|
@@ -4597,6 +4109,22 @@ class DevBridge {
|
|
|
4597
4109
|
}
|
|
4598
4110
|
}
|
|
4599
4111
|
|
|
4112
|
+
Object.defineProperty(exports, "ButtonContainer", {
|
|
4113
|
+
enumerable: true,
|
|
4114
|
+
get: function () { return ui.ButtonContainer; }
|
|
4115
|
+
});
|
|
4116
|
+
Object.defineProperty(exports, "FancyButton", {
|
|
4117
|
+
enumerable: true,
|
|
4118
|
+
get: function () { return ui.FancyButton; }
|
|
4119
|
+
});
|
|
4120
|
+
Object.defineProperty(exports, "ScrollBox", {
|
|
4121
|
+
enumerable: true,
|
|
4122
|
+
get: function () { return ui.ScrollBox; }
|
|
4123
|
+
});
|
|
4124
|
+
Object.defineProperty(exports, "LayoutContainer", {
|
|
4125
|
+
enumerable: true,
|
|
4126
|
+
get: function () { return components.LayoutContainer; }
|
|
4127
|
+
});
|
|
4600
4128
|
exports.AssetManager = AssetManager;
|
|
4601
4129
|
exports.AudioManager = AudioManager;
|
|
4602
4130
|
exports.BalanceDisplay = BalanceDisplay;
|