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