@energy8platform/game-engine 0.3.0 → 0.4.0

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