@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.cjs.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var pixi_js = require('pixi.js');
|
|
4
4
|
var gameSdk = require('@energy8platform/game-sdk');
|
|
5
|
+
var ui = require('@pixi/ui');
|
|
6
|
+
var components = require('@pixi/layout/components');
|
|
5
7
|
|
|
6
8
|
// ─── Scale Modes ───────────────────────────────────────────
|
|
7
9
|
exports.ScaleMode = void 0;
|
|
@@ -2174,6 +2176,7 @@ class GameApplication extends EventEmitter {
|
|
|
2174
2176
|
async initPixi() {
|
|
2175
2177
|
this.app = new pixi_js.Application();
|
|
2176
2178
|
const pixiOpts = {
|
|
2179
|
+
preference: 'webgl',
|
|
2177
2180
|
background: typeof this.config.loading?.backgroundColor === 'number'
|
|
2178
2181
|
? this.config.loading.backgroundColor
|
|
2179
2182
|
: 0x000000,
|
|
@@ -2881,161 +2884,112 @@ class SpriteAnimation {
|
|
|
2881
2884
|
}
|
|
2882
2885
|
|
|
2883
2886
|
const DEFAULT_COLORS = {
|
|
2884
|
-
|
|
2887
|
+
default: 0xffd700,
|
|
2885
2888
|
hover: 0xffe44d,
|
|
2886
2889
|
pressed: 0xccac00,
|
|
2887
2890
|
disabled: 0x666666,
|
|
2888
2891
|
};
|
|
2892
|
+
function makeGraphicsView(w, h, radius, color) {
|
|
2893
|
+
const g = new pixi_js.Graphics();
|
|
2894
|
+
g.roundRect(0, 0, w, h, radius).fill(color);
|
|
2895
|
+
// Highlight overlay
|
|
2896
|
+
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
2897
|
+
return g;
|
|
2898
|
+
}
|
|
2889
2899
|
/**
|
|
2890
|
-
* Interactive button component
|
|
2900
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
2891
2901
|
*
|
|
2892
|
-
* Supports both texture-based and Graphics-based rendering
|
|
2902
|
+
* Supports both texture-based and Graphics-based rendering with
|
|
2903
|
+
* per-state views, press animation, and text.
|
|
2893
2904
|
*
|
|
2894
2905
|
* @example
|
|
2895
2906
|
* ```ts
|
|
2896
2907
|
* const btn = new Button({
|
|
2897
2908
|
* width: 200, height: 60, borderRadius: 12,
|
|
2898
|
-
* colors: {
|
|
2909
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
2910
|
+
* text: 'SPIN',
|
|
2899
2911
|
* });
|
|
2900
2912
|
*
|
|
2901
|
-
* btn.
|
|
2913
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
2902
2914
|
* scene.container.addChild(btn);
|
|
2903
2915
|
* ```
|
|
2904
2916
|
*/
|
|
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;
|
|
2917
|
+
class Button extends ui.FancyButton {
|
|
2918
|
+
_buttonConfig;
|
|
2914
2919
|
constructor(config = {}) {
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
animationDuration: 100,
|
|
2920
|
+
const resolvedConfig = {
|
|
2921
|
+
width: config.width ?? 200,
|
|
2922
|
+
height: config.height ?? 60,
|
|
2923
|
+
borderRadius: config.borderRadius ?? 8,
|
|
2924
|
+
pressScale: config.pressScale ?? 0.95,
|
|
2925
|
+
animationDuration: config.animationDuration ?? 100,
|
|
2922
2926
|
...config,
|
|
2923
2927
|
};
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
2929
|
+
const { width, height, borderRadius } = resolvedConfig;
|
|
2930
|
+
// Build FancyButton options
|
|
2931
|
+
const options = {
|
|
2932
|
+
anchor: 0.5,
|
|
2933
|
+
animations: {
|
|
2934
|
+
hover: {
|
|
2935
|
+
props: { scale: { x: 1.03, y: 1.03 } },
|
|
2936
|
+
duration: resolvedConfig.animationDuration,
|
|
2937
|
+
},
|
|
2938
|
+
pressed: {
|
|
2939
|
+
props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
|
|
2940
|
+
duration: resolvedConfig.animationDuration,
|
|
2941
|
+
},
|
|
2942
|
+
},
|
|
2943
|
+
};
|
|
2944
|
+
// Texture-based views
|
|
2928
2945
|
if (config.textures) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2946
|
+
if (config.textures.default)
|
|
2947
|
+
options.defaultView = config.textures.default;
|
|
2948
|
+
if (config.textures.hover)
|
|
2949
|
+
options.hoverView = config.textures.hover;
|
|
2950
|
+
if (config.textures.pressed)
|
|
2951
|
+
options.pressedView = config.textures.pressed;
|
|
2952
|
+
if (config.textures.disabled)
|
|
2953
|
+
options.disabledView = config.textures.disabled;
|
|
2937
2954
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
this.
|
|
2955
|
+
else {
|
|
2956
|
+
// Graphics-based views
|
|
2957
|
+
options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
|
|
2958
|
+
options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
|
|
2959
|
+
options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
|
|
2960
|
+
options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
|
|
2961
|
+
}
|
|
2962
|
+
// Text
|
|
2963
|
+
if (config.text) {
|
|
2964
|
+
options.text = config.text;
|
|
2965
|
+
}
|
|
2966
|
+
super(options);
|
|
2967
|
+
this._buttonConfig = resolvedConfig;
|
|
2951
2968
|
if (config.disabled) {
|
|
2952
|
-
this.
|
|
2969
|
+
this.enabled = false;
|
|
2953
2970
|
}
|
|
2954
2971
|
}
|
|
2955
|
-
/** Current button state */
|
|
2956
|
-
get state() {
|
|
2957
|
-
return this._state;
|
|
2958
|
-
}
|
|
2959
2972
|
/** Enable the button */
|
|
2960
2973
|
enable() {
|
|
2961
|
-
|
|
2962
|
-
this.setState('normal');
|
|
2963
|
-
this.eventMode = 'static';
|
|
2964
|
-
this.cursor = 'pointer';
|
|
2965
|
-
}
|
|
2974
|
+
this.enabled = true;
|
|
2966
2975
|
}
|
|
2967
2976
|
/** Disable the button */
|
|
2968
2977
|
disable() {
|
|
2969
|
-
this.
|
|
2970
|
-
this.eventMode = 'none';
|
|
2971
|
-
this.cursor = 'default';
|
|
2978
|
+
this.enabled = false;
|
|
2972
2979
|
}
|
|
2973
2980
|
/** Whether the button is disabled */
|
|
2974
2981
|
get disabled() {
|
|
2975
|
-
return this.
|
|
2976
|
-
}
|
|
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
|
-
}
|
|
2982
|
+
return !this.enabled;
|
|
3005
2983
|
}
|
|
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
2984
|
}
|
|
3036
2985
|
|
|
2986
|
+
function makeBarGraphics(w, h, radius, color) {
|
|
2987
|
+
return new pixi_js.Graphics().roundRect(0, 0, w, h, radius).fill(color);
|
|
2988
|
+
}
|
|
3037
2989
|
/**
|
|
3038
|
-
* Horizontal progress bar
|
|
2990
|
+
* Horizontal progress bar powered by `@pixi/ui` ProgressBar.
|
|
2991
|
+
*
|
|
2992
|
+
* Provides optional smooth animated fill via per-frame `update()`.
|
|
3039
2993
|
*
|
|
3040
2994
|
* @example
|
|
3041
2995
|
* ```ts
|
|
@@ -3045,33 +2999,48 @@ class Button extends pixi_js.Container {
|
|
|
3045
2999
|
* ```
|
|
3046
3000
|
*/
|
|
3047
3001
|
class ProgressBar extends pixi_js.Container {
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
_border;
|
|
3002
|
+
_bar;
|
|
3003
|
+
_borderGfx;
|
|
3051
3004
|
_config;
|
|
3052
3005
|
_progress = 0;
|
|
3053
3006
|
_displayedProgress = 0;
|
|
3054
3007
|
constructor(config = {}) {
|
|
3055
3008
|
super();
|
|
3056
3009
|
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
|
-
|
|
3010
|
+
width: config.width ?? 300,
|
|
3011
|
+
height: config.height ?? 16,
|
|
3012
|
+
borderRadius: config.borderRadius ?? 8,
|
|
3013
|
+
fillColor: config.fillColor ?? 0xffd700,
|
|
3014
|
+
trackColor: config.trackColor ?? 0x333333,
|
|
3015
|
+
borderColor: config.borderColor ?? 0x555555,
|
|
3016
|
+
borderWidth: config.borderWidth ?? 1,
|
|
3017
|
+
animated: config.animated ?? true,
|
|
3018
|
+
animationSpeed: config.animationSpeed ?? 0.1,
|
|
3019
|
+
};
|
|
3020
|
+
const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
|
|
3021
|
+
const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
|
|
3022
|
+
const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
|
|
3023
|
+
const options = {
|
|
3024
|
+
bg: bgGraphics,
|
|
3025
|
+
fill: fillGraphics,
|
|
3026
|
+
fillPaddings: {
|
|
3027
|
+
top: borderWidth,
|
|
3028
|
+
right: borderWidth,
|
|
3029
|
+
bottom: borderWidth,
|
|
3030
|
+
left: borderWidth,
|
|
3031
|
+
},
|
|
3032
|
+
progress: 0,
|
|
3067
3033
|
};
|
|
3068
|
-
this.
|
|
3069
|
-
this.
|
|
3070
|
-
|
|
3071
|
-
this.
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3034
|
+
this._bar = new ui.ProgressBar(options);
|
|
3035
|
+
this.addChild(this._bar);
|
|
3036
|
+
// Border overlay
|
|
3037
|
+
this._borderGfx = new pixi_js.Graphics();
|
|
3038
|
+
if (borderColor !== undefined && borderWidth > 0) {
|
|
3039
|
+
this._borderGfx
|
|
3040
|
+
.roundRect(0, 0, width, height, borderRadius)
|
|
3041
|
+
.stroke({ color: borderColor, width: borderWidth });
|
|
3042
|
+
}
|
|
3043
|
+
this.addChild(this._borderGfx);
|
|
3075
3044
|
}
|
|
3076
3045
|
/** Get/set progress (0..1) */
|
|
3077
3046
|
get progress() {
|
|
@@ -3081,13 +3050,13 @@ class ProgressBar extends pixi_js.Container {
|
|
|
3081
3050
|
this._progress = Math.max(0, Math.min(1, value));
|
|
3082
3051
|
if (!this._config.animated) {
|
|
3083
3052
|
this._displayedProgress = this._progress;
|
|
3084
|
-
this.
|
|
3053
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
3085
3054
|
}
|
|
3086
3055
|
}
|
|
3087
3056
|
/**
|
|
3088
3057
|
* Call each frame if animated is true.
|
|
3089
3058
|
*/
|
|
3090
|
-
update(
|
|
3059
|
+
update(_dt) {
|
|
3091
3060
|
if (!this._config.animated)
|
|
3092
3061
|
return;
|
|
3093
3062
|
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
@@ -3096,35 +3065,7 @@ class ProgressBar extends pixi_js.Container {
|
|
|
3096
3065
|
}
|
|
3097
3066
|
this._displayedProgress +=
|
|
3098
3067
|
(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
|
-
}
|
|
3068
|
+
this._bar.progress = this._displayedProgress * 100;
|
|
3128
3069
|
}
|
|
3129
3070
|
}
|
|
3130
3071
|
|
|
@@ -3220,7 +3161,10 @@ class Label extends pixi_js.Container {
|
|
|
3220
3161
|
}
|
|
3221
3162
|
|
|
3222
3163
|
/**
|
|
3223
|
-
* Background panel
|
|
3164
|
+
* Background panel powered by `@pixi/layout` LayoutContainer.
|
|
3165
|
+
*
|
|
3166
|
+
* Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
|
|
3167
|
+
* Children added to `content` participate in flexbox layout automatically.
|
|
3224
3168
|
*
|
|
3225
3169
|
* @example
|
|
3226
3170
|
* ```ts
|
|
@@ -3235,72 +3179,67 @@ class Label extends pixi_js.Container {
|
|
|
3235
3179
|
* });
|
|
3236
3180
|
* ```
|
|
3237
3181
|
*/
|
|
3238
|
-
class Panel extends
|
|
3239
|
-
|
|
3240
|
-
_content;
|
|
3241
|
-
_config;
|
|
3182
|
+
class Panel extends components.LayoutContainer {
|
|
3183
|
+
_panelConfig;
|
|
3242
3184
|
constructor(config = {}) {
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
backgroundAlpha: 1,
|
|
3185
|
+
const resolvedConfig = {
|
|
3186
|
+
width: config.width ?? 400,
|
|
3187
|
+
height: config.height ?? 300,
|
|
3188
|
+
padding: config.padding ?? 16,
|
|
3189
|
+
backgroundAlpha: config.backgroundAlpha ?? 1,
|
|
3249
3190
|
...config,
|
|
3250
3191
|
};
|
|
3251
|
-
//
|
|
3192
|
+
// If using a 9-slice texture, pass it as a custom background
|
|
3193
|
+
let customBackground;
|
|
3252
3194
|
if (config.nineSliceTexture) {
|
|
3253
3195
|
const texture = typeof config.nineSliceTexture === 'string'
|
|
3254
3196
|
? pixi_js.Texture.from(config.nineSliceTexture)
|
|
3255
3197
|
: config.nineSliceTexture;
|
|
3256
3198
|
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
3257
|
-
|
|
3199
|
+
const nineSlice = new pixi_js.NineSliceSprite({
|
|
3258
3200
|
texture,
|
|
3259
3201
|
leftWidth: left,
|
|
3260
3202
|
topHeight: top,
|
|
3261
3203
|
rightWidth: right,
|
|
3262
3204
|
bottomHeight: bottom,
|
|
3263
3205
|
});
|
|
3264
|
-
|
|
3265
|
-
|
|
3206
|
+
nineSlice.width = resolvedConfig.width;
|
|
3207
|
+
nineSlice.height = resolvedConfig.height;
|
|
3208
|
+
nineSlice.alpha = resolvedConfig.backgroundAlpha;
|
|
3209
|
+
customBackground = nineSlice;
|
|
3210
|
+
}
|
|
3211
|
+
super(customBackground ? { background: customBackground } : undefined);
|
|
3212
|
+
this._panelConfig = resolvedConfig;
|
|
3213
|
+
// Apply layout styles
|
|
3214
|
+
const layoutStyles = {
|
|
3215
|
+
width: resolvedConfig.width,
|
|
3216
|
+
height: resolvedConfig.height,
|
|
3217
|
+
padding: resolvedConfig.padding,
|
|
3218
|
+
flexDirection: 'column',
|
|
3219
|
+
};
|
|
3220
|
+
// Graphics-based background via layout styles
|
|
3221
|
+
if (!config.nineSliceTexture) {
|
|
3222
|
+
layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
|
|
3223
|
+
layoutStyles.borderRadius = config.borderRadius ?? 0;
|
|
3224
|
+
if (config.borderColor !== undefined && config.borderWidth) {
|
|
3225
|
+
layoutStyles.borderColor = config.borderColor;
|
|
3226
|
+
layoutStyles.borderWidth = config.borderWidth;
|
|
3227
|
+
}
|
|
3266
3228
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
this.
|
|
3229
|
+
this.layout = layoutStyles;
|
|
3230
|
+
if (!config.nineSliceTexture) {
|
|
3231
|
+
this.background.alpha = resolvedConfig.backgroundAlpha;
|
|
3270
3232
|
}
|
|
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
3233
|
}
|
|
3279
|
-
/**
|
|
3234
|
+
/** Access the content container (children added here participate in layout) */
|
|
3280
3235
|
get content() {
|
|
3281
|
-
return this.
|
|
3236
|
+
return this.overflowContainer;
|
|
3282
3237
|
}
|
|
3283
3238
|
/** Resize the panel */
|
|
3284
3239
|
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
|
-
}
|
|
3240
|
+
this._panelConfig.width = width;
|
|
3241
|
+
this._panelConfig.height = height;
|
|
3242
|
+
this._layout?.setStyle({ width, height });
|
|
3304
3243
|
}
|
|
3305
3244
|
}
|
|
3306
3245
|
|
|
@@ -3388,7 +3327,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
3388
3327
|
this.updateDisplay();
|
|
3389
3328
|
}
|
|
3390
3329
|
async animateValue(from, to) {
|
|
3391
|
-
// Cancel any ongoing animation
|
|
3392
3330
|
if (this._animating) {
|
|
3393
3331
|
this._animationCancelled = true;
|
|
3394
3332
|
}
|
|
@@ -3398,7 +3336,6 @@ class BalanceDisplay extends pixi_js.Container {
|
|
|
3398
3336
|
const startTime = Date.now();
|
|
3399
3337
|
return new Promise((resolve) => {
|
|
3400
3338
|
const tick = () => {
|
|
3401
|
-
// If cancelled by a newer animation, stop immediately
|
|
3402
3339
|
if (this._animationCancelled) {
|
|
3403
3340
|
this._animating = false;
|
|
3404
3341
|
resolve();
|
|
@@ -3540,6 +3477,8 @@ class WinDisplay extends pixi_js.Container {
|
|
|
3540
3477
|
* Modal overlay component.
|
|
3541
3478
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
3542
3479
|
*
|
|
3480
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
3481
|
+
*
|
|
3543
3482
|
* @example
|
|
3544
3483
|
* ```ts
|
|
3545
3484
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -3558,11 +3497,10 @@ class Modal extends pixi_js.Container {
|
|
|
3558
3497
|
constructor(config = {}) {
|
|
3559
3498
|
super();
|
|
3560
3499
|
this._config = {
|
|
3561
|
-
overlayColor: 0x000000,
|
|
3562
|
-
overlayAlpha: 0.7,
|
|
3563
|
-
closeOnOverlay: true,
|
|
3564
|
-
animationDuration: 300,
|
|
3565
|
-
...config,
|
|
3500
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
3501
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
3502
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
3503
|
+
animationDuration: config.animationDuration ?? 300,
|
|
3566
3504
|
};
|
|
3567
3505
|
// Overlay
|
|
3568
3506
|
this._overlay = new pixi_js.Graphics();
|
|
@@ -3645,9 +3583,8 @@ class Toast extends pixi_js.Container {
|
|
|
3645
3583
|
constructor(config = {}) {
|
|
3646
3584
|
super();
|
|
3647
3585
|
this._config = {
|
|
3648
|
-
duration: 3000,
|
|
3649
|
-
bottomOffset: 60,
|
|
3650
|
-
...config,
|
|
3586
|
+
duration: config.duration ?? 3000,
|
|
3587
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
3651
3588
|
};
|
|
3652
3589
|
this._bg = new pixi_js.Graphics();
|
|
3653
3590
|
this.addChild(this._bg);
|
|
@@ -3667,7 +3604,6 @@ class Toast extends pixi_js.Container {
|
|
|
3667
3604
|
* Show a toast message.
|
|
3668
3605
|
*/
|
|
3669
3606
|
async show(message, type = 'info', viewWidth, viewHeight) {
|
|
3670
|
-
// Clear previous dismiss
|
|
3671
3607
|
if (this._dismissTimeout) {
|
|
3672
3608
|
clearTimeout(this._dismissTimeout);
|
|
3673
3609
|
}
|
|
@@ -3676,10 +3612,10 @@ class Toast extends pixi_js.Container {
|
|
|
3676
3612
|
const width = Math.max(200, this._text.width + padding * 2);
|
|
3677
3613
|
const height = 44;
|
|
3678
3614
|
const radius = 8;
|
|
3615
|
+
// Draw the background
|
|
3679
3616
|
this._bg.clear();
|
|
3680
|
-
this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
|
|
3681
|
-
this._bg.
|
|
3682
|
-
.fill({ color: 0x000000, alpha: 0.2 });
|
|
3617
|
+
this._bg.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
3618
|
+
this._bg.fill(TOAST_COLORS[type]);
|
|
3683
3619
|
// Position
|
|
3684
3620
|
if (viewWidth && viewHeight) {
|
|
3685
3621
|
this.x = viewWidth / 2;
|
|
@@ -3688,9 +3624,7 @@ class Toast extends pixi_js.Container {
|
|
|
3688
3624
|
this.visible = true;
|
|
3689
3625
|
this.alpha = 0;
|
|
3690
3626
|
this.y += 20;
|
|
3691
|
-
// Animate in
|
|
3692
3627
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
3693
|
-
// Auto-dismiss
|
|
3694
3628
|
if (this._config.duration > 0) {
|
|
3695
3629
|
this._dismissTimeout = setTimeout(() => {
|
|
3696
3630
|
this.dismiss();
|
|
@@ -3712,8 +3646,48 @@ class Toast extends pixi_js.Container {
|
|
|
3712
3646
|
}
|
|
3713
3647
|
}
|
|
3714
3648
|
|
|
3649
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
3650
|
+
const ALIGNMENT_MAP = {
|
|
3651
|
+
start: 'flex-start',
|
|
3652
|
+
center: 'center',
|
|
3653
|
+
end: 'flex-end',
|
|
3654
|
+
stretch: 'stretch',
|
|
3655
|
+
};
|
|
3656
|
+
function normalizePadding(padding) {
|
|
3657
|
+
if (typeof padding === 'number')
|
|
3658
|
+
return [padding, padding, padding, padding];
|
|
3659
|
+
return padding;
|
|
3660
|
+
}
|
|
3661
|
+
function directionToFlexStyles(direction, maxWidth) {
|
|
3662
|
+
switch (direction) {
|
|
3663
|
+
case 'horizontal':
|
|
3664
|
+
return { flexDirection: 'row', flexWrap: 'nowrap' };
|
|
3665
|
+
case 'vertical':
|
|
3666
|
+
return { flexDirection: 'column', flexWrap: 'nowrap' };
|
|
3667
|
+
case 'grid':
|
|
3668
|
+
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
3669
|
+
case 'wrap':
|
|
3670
|
+
return {
|
|
3671
|
+
flexDirection: 'row',
|
|
3672
|
+
flexWrap: 'wrap',
|
|
3673
|
+
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
function buildLayoutStyles(config) {
|
|
3678
|
+
const [pt, pr, pb, pl] = config.padding;
|
|
3679
|
+
return {
|
|
3680
|
+
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
3681
|
+
gap: config.gap,
|
|
3682
|
+
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
3683
|
+
paddingTop: pt,
|
|
3684
|
+
paddingRight: pr,
|
|
3685
|
+
paddingBottom: pb,
|
|
3686
|
+
paddingLeft: pl,
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3715
3689
|
/**
|
|
3716
|
-
* Responsive layout container
|
|
3690
|
+
* Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
|
|
3717
3691
|
*
|
|
3718
3692
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
3719
3693
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -3736,47 +3710,44 @@ class Toast extends pixi_js.Container {
|
|
|
3736
3710
|
* toolbar.addItem(betLabel);
|
|
3737
3711
|
* scene.container.addChild(toolbar);
|
|
3738
3712
|
*
|
|
3739
|
-
* // On resize, update layout position relative to viewport
|
|
3740
3713
|
* toolbar.updateViewport(width, height);
|
|
3741
3714
|
* ```
|
|
3742
3715
|
*/
|
|
3743
3716
|
class Layout extends pixi_js.Container {
|
|
3744
|
-
|
|
3717
|
+
_layoutConfig;
|
|
3745
3718
|
_padding;
|
|
3746
3719
|
_anchor;
|
|
3747
3720
|
_maxWidth;
|
|
3748
3721
|
_breakpoints;
|
|
3749
|
-
_content;
|
|
3750
3722
|
_items = [];
|
|
3751
3723
|
_viewportWidth = 0;
|
|
3752
3724
|
_viewportHeight = 0;
|
|
3753
3725
|
constructor(config = {}) {
|
|
3754
3726
|
super();
|
|
3755
|
-
this.
|
|
3727
|
+
this._layoutConfig = {
|
|
3756
3728
|
direction: config.direction ?? 'vertical',
|
|
3757
3729
|
gap: config.gap ?? 0,
|
|
3758
3730
|
alignment: config.alignment ?? 'start',
|
|
3759
3731
|
autoLayout: config.autoLayout ?? true,
|
|
3760
3732
|
columns: config.columns ?? 2,
|
|
3761
3733
|
};
|
|
3762
|
-
this._padding =
|
|
3734
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
3763
3735
|
this._anchor = config.anchor ?? 'top-left';
|
|
3764
3736
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3765
|
-
// Sort breakpoints by width ascending for correct resolution
|
|
3766
3737
|
this._breakpoints = config.breakpoints
|
|
3767
3738
|
? Object.entries(config.breakpoints)
|
|
3768
3739
|
.map(([w, cfg]) => [Number(w), cfg])
|
|
3769
3740
|
.sort((a, b) => a[0] - b[0])
|
|
3770
3741
|
: [];
|
|
3771
|
-
this.
|
|
3772
|
-
this.addChild(this._content);
|
|
3742
|
+
this.applyLayoutStyles();
|
|
3773
3743
|
}
|
|
3774
3744
|
/** Add an item to the layout */
|
|
3775
3745
|
addItem(child) {
|
|
3776
3746
|
this._items.push(child);
|
|
3777
|
-
this.
|
|
3778
|
-
if (this.
|
|
3779
|
-
this.
|
|
3747
|
+
this.addChild(child);
|
|
3748
|
+
if (this._layoutConfig.direction === 'grid') {
|
|
3749
|
+
this.applyGridChildWidth(child);
|
|
3750
|
+
}
|
|
3780
3751
|
return this;
|
|
3781
3752
|
}
|
|
3782
3753
|
/** Remove an item from the layout */
|
|
@@ -3784,20 +3755,16 @@ class Layout extends pixi_js.Container {
|
|
|
3784
3755
|
const idx = this._items.indexOf(child);
|
|
3785
3756
|
if (idx !== -1) {
|
|
3786
3757
|
this._items.splice(idx, 1);
|
|
3787
|
-
this.
|
|
3788
|
-
if (this._config.autoLayout)
|
|
3789
|
-
this.layout();
|
|
3758
|
+
this.removeChild(child);
|
|
3790
3759
|
}
|
|
3791
3760
|
return this;
|
|
3792
3761
|
}
|
|
3793
3762
|
/** Remove all items */
|
|
3794
3763
|
clearItems() {
|
|
3795
3764
|
for (const item of this._items) {
|
|
3796
|
-
this.
|
|
3765
|
+
this.removeChild(item);
|
|
3797
3766
|
}
|
|
3798
3767
|
this._items.length = 0;
|
|
3799
|
-
if (this._config.autoLayout)
|
|
3800
|
-
this.layout();
|
|
3801
3768
|
return this;
|
|
3802
3769
|
}
|
|
3803
3770
|
/** Get all layout items */
|
|
@@ -3811,129 +3778,55 @@ class Layout extends pixi_js.Container {
|
|
|
3811
3778
|
updateViewport(width, height) {
|
|
3812
3779
|
this._viewportWidth = width;
|
|
3813
3780
|
this._viewportHeight = height;
|
|
3814
|
-
this.
|
|
3781
|
+
this.applyLayoutStyles();
|
|
3782
|
+
this.applyAnchor();
|
|
3815
3783
|
}
|
|
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)
|
|
3784
|
+
applyLayoutStyles() {
|
|
3823
3785
|
const effective = this.resolveConfig();
|
|
3824
|
-
const
|
|
3825
|
-
const
|
|
3826
|
-
const alignment = effective.alignment ?? this.
|
|
3827
|
-
|
|
3786
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
3787
|
+
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3788
|
+
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
3789
|
+
effective.columns ?? this._layoutConfig.columns;
|
|
3828
3790
|
const padding = effective.padding !== undefined
|
|
3829
|
-
?
|
|
3791
|
+
? normalizePadding(effective.padding)
|
|
3830
3792
|
: this._padding;
|
|
3831
3793
|
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;
|
|
3794
|
+
const styles = buildLayoutStyles({ direction, gap, alignment, padding, maxWidth });
|
|
3795
|
+
this.layout = styles;
|
|
3796
|
+
if (direction === 'grid') {
|
|
3797
|
+
for (const item of this._items) {
|
|
3798
|
+
this.applyGridChildWidth(item);
|
|
3919
3799
|
}
|
|
3920
|
-
item.x = x;
|
|
3921
|
-
item.y = y;
|
|
3922
|
-
x += size.width + gap;
|
|
3923
|
-
rowHeight = Math.max(rowHeight, size.height);
|
|
3924
3800
|
}
|
|
3925
3801
|
}
|
|
3926
|
-
|
|
3802
|
+
applyGridChildWidth(child) {
|
|
3803
|
+
const effective = this.resolveConfig();
|
|
3804
|
+
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
3805
|
+
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
3806
|
+
// Account for gaps between columns: total gap space = gap * (columns - 1)
|
|
3807
|
+
// Each column gets: (100% - total_gap) / columns
|
|
3808
|
+
// We use flexBasis + flexGrow to let Yoga handle the math when gap > 0
|
|
3809
|
+
const styles = gap > 0
|
|
3810
|
+
? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
|
|
3811
|
+
: { width: `${(100 / columns).toFixed(2)}%` };
|
|
3812
|
+
if (child._layout) {
|
|
3813
|
+
child._layout.setStyle(styles);
|
|
3814
|
+
}
|
|
3815
|
+
else {
|
|
3816
|
+
child.layout = styles;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
applyAnchor() {
|
|
3820
|
+
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
3927
3821
|
if (this._viewportWidth === 0 || this._viewportHeight === 0)
|
|
3928
3822
|
return;
|
|
3929
|
-
const bounds = this.
|
|
3930
|
-
const contentW = bounds.width;
|
|
3931
|
-
const contentH = bounds.height;
|
|
3823
|
+
const bounds = this.getLocalBounds();
|
|
3824
|
+
const contentW = bounds.width * this.scale.x;
|
|
3825
|
+
const contentH = bounds.height * this.scale.y;
|
|
3932
3826
|
const vw = this._viewportWidth;
|
|
3933
3827
|
const vh = this._viewportHeight;
|
|
3934
3828
|
let anchorX = 0;
|
|
3935
3829
|
let anchorY = 0;
|
|
3936
|
-
// Horizontal
|
|
3937
3830
|
if (anchor.includes('left')) {
|
|
3938
3831
|
anchorX = 0;
|
|
3939
3832
|
}
|
|
@@ -3941,10 +3834,8 @@ class Layout extends pixi_js.Container {
|
|
|
3941
3834
|
anchorX = vw - contentW;
|
|
3942
3835
|
}
|
|
3943
3836
|
else {
|
|
3944
|
-
// center
|
|
3945
3837
|
anchorX = (vw - contentW) / 2;
|
|
3946
3838
|
}
|
|
3947
|
-
// Vertical
|
|
3948
3839
|
if (anchor.startsWith('top')) {
|
|
3949
3840
|
anchorY = 0;
|
|
3950
3841
|
}
|
|
@@ -3952,44 +3843,34 @@ class Layout extends pixi_js.Container {
|
|
|
3952
3843
|
anchorY = vh - contentH;
|
|
3953
3844
|
}
|
|
3954
3845
|
else {
|
|
3955
|
-
// center
|
|
3956
3846
|
anchorY = (vh - contentH) / 2;
|
|
3957
3847
|
}
|
|
3958
|
-
|
|
3959
|
-
this.
|
|
3960
|
-
this.y = anchorY - bounds.y;
|
|
3848
|
+
this.x = anchorX - bounds.x * this.scale.x;
|
|
3849
|
+
this.y = anchorY - bounds.y * this.scale.y;
|
|
3961
3850
|
}
|
|
3962
3851
|
resolveConfig() {
|
|
3963
3852
|
if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
|
|
3964
3853
|
return {};
|
|
3965
3854
|
}
|
|
3966
|
-
// Find the largest breakpoint that's ≤ current viewport width
|
|
3967
|
-
let resolved = {};
|
|
3968
3855
|
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
3969
3856
|
if (this._viewportWidth <= maxWidth) {
|
|
3970
|
-
|
|
3971
|
-
break;
|
|
3857
|
+
return overrides;
|
|
3972
3858
|
}
|
|
3973
3859
|
}
|
|
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;
|
|
3860
|
+
return {};
|
|
3985
3861
|
}
|
|
3986
3862
|
}
|
|
3987
3863
|
|
|
3864
|
+
const DIRECTION_MAP = {
|
|
3865
|
+
vertical: 'vertical',
|
|
3866
|
+
horizontal: 'horizontal',
|
|
3867
|
+
both: 'bidirectional',
|
|
3868
|
+
};
|
|
3988
3869
|
/**
|
|
3989
|
-
* Scrollable container
|
|
3870
|
+
* Scrollable container powered by `@pixi/ui` ScrollBox.
|
|
3990
3871
|
*
|
|
3991
|
-
*
|
|
3992
|
-
*
|
|
3872
|
+
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
3873
|
+
* dynamic rendering optimization for off-screen items.
|
|
3993
3874
|
*
|
|
3994
3875
|
* @example
|
|
3995
3876
|
* ```ts
|
|
@@ -3997,435 +3878,63 @@ class Layout extends pixi_js.Container {
|
|
|
3997
3878
|
* width: 600,
|
|
3998
3879
|
* height: 400,
|
|
3999
3880
|
* direction: 'vertical',
|
|
4000
|
-
*
|
|
4001
|
-
* elasticity: 0.3,
|
|
3881
|
+
* elementsMargin: 8,
|
|
4002
3882
|
* });
|
|
4003
3883
|
*
|
|
4004
|
-
* // Add content taller than 400px
|
|
4005
|
-
* const list = new Container();
|
|
4006
3884
|
* for (let i = 0; i < 50; i++) {
|
|
4007
|
-
*
|
|
4008
|
-
* row.y = i * 40;
|
|
4009
|
-
* list.addChild(row);
|
|
3885
|
+
* scroll.addItem(createRow(i));
|
|
4010
3886
|
* }
|
|
4011
|
-
* scroll.setContent(list);
|
|
4012
3887
|
*
|
|
4013
3888
|
* scene.container.addChild(scroll);
|
|
4014
3889
|
* ```
|
|
4015
3890
|
*/
|
|
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;
|
|
3891
|
+
class ScrollContainer extends ui.ScrollBox {
|
|
3892
|
+
_scrollConfig;
|
|
4037
3893
|
constructor(config) {
|
|
4038
|
-
|
|
4039
|
-
this._config = {
|
|
3894
|
+
const options = {
|
|
4040
3895
|
width: config.width,
|
|
4041
3896
|
height: config.height,
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
snapSize: config.snapSize ?? 0,
|
|
4050
|
-
borderRadius: config.borderRadius ?? 0,
|
|
3897
|
+
type: DIRECTION_MAP[config.direction ?? 'vertical'],
|
|
3898
|
+
radius: config.borderRadius ?? 0,
|
|
3899
|
+
elementsMargin: config.elementsMargin ?? 0,
|
|
3900
|
+
padding: config.padding ?? 0,
|
|
3901
|
+
disableDynamicRendering: config.disableDynamicRendering ?? false,
|
|
3902
|
+
disableEasing: config.disableEasing ?? false,
|
|
3903
|
+
globalScroll: config.globalScroll ?? true,
|
|
4051
3904
|
};
|
|
4052
|
-
// Background
|
|
4053
|
-
this._bg = new pixi_js.Graphics();
|
|
4054
3905
|
if (config.backgroundColor !== undefined) {
|
|
4055
|
-
|
|
4056
|
-
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
4057
|
-
}
|
|
4058
|
-
this.addChild(this._bg);
|
|
4059
|
-
// Viewport (masked area)
|
|
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
|
-
}
|
|
3906
|
+
options.background = config.backgroundColor;
|
|
4080
3907
|
}
|
|
4081
|
-
|
|
4082
|
-
this.
|
|
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);
|
|
3908
|
+
super(options);
|
|
3909
|
+
this._scrollConfig = config;
|
|
4090
3910
|
}
|
|
4091
3911
|
/** Set scrollable content. Replaces any existing content. */
|
|
4092
3912
|
setContent(content) {
|
|
4093
|
-
|
|
4094
|
-
|
|
3913
|
+
// Remove existing items
|
|
3914
|
+
const existing = this.items;
|
|
3915
|
+
if (existing.length > 0) {
|
|
3916
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
3917
|
+
this.removeItem(i);
|
|
3918
|
+
}
|
|
4095
3919
|
}
|
|
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;
|
|
3920
|
+
// Add all children from the content container
|
|
3921
|
+
const children = [...content.children];
|
|
3922
|
+
if (children.length > 0) {
|
|
3923
|
+
this.addItems(children);
|
|
4114
3924
|
}
|
|
4115
|
-
|
|
3925
|
+
}
|
|
3926
|
+
/** Add a single item */
|
|
3927
|
+
addItem(...items) {
|
|
3928
|
+
this.addItems(items);
|
|
3929
|
+
return items[0];
|
|
4116
3930
|
}
|
|
4117
3931
|
/** Scroll to make a specific item/child visible */
|
|
4118
3932
|
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
|
-
}
|
|
3933
|
+
this.scrollTo(index);
|
|
4128
3934
|
}
|
|
4129
3935
|
/** Current scroll position */
|
|
4130
3936
|
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);
|
|
3937
|
+
return { x: this.scrollX, y: this.scrollY };
|
|
4429
3938
|
}
|
|
4430
3939
|
}
|
|
4431
3940
|
|