@energy8platform/game-engine 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -44
- package/dist/core.cjs.js +1 -0
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +1 -0
- package/dist/core.esm.js.map +1 -1
- package/dist/index.cjs.js +317 -789
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +61 -129
- package/dist/index.esm.js +304 -790
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +637 -1106
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +60 -128
- package/dist/ui.esm.js +620 -1107
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +23 -3
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +23 -3
- package/dist/vite.esm.js.map +1 -1
- package/package.json +17 -2
- package/src/core/GameApplication.ts +1 -0
- package/src/index.ts +16 -0
- package/src/ui/BalanceDisplay.ts +0 -3
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +102 -180
- package/src/ui/Modal.ts +6 -5
- package/src/ui/Panel.ts +52 -55
- package/src/ui/ProgressBar.ts +52 -57
- package/src/ui/ScrollContainer.ts +58 -489
- package/src/ui/Toast.ts +19 -13
- package/src/ui/index.ts +13 -0
- package/src/vite/index.ts +23 -3
package/src/ui/Button.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Graphics, Texture } from 'pixi.js';
|
|
2
|
+
import { FancyButton } from '@pixi/ui';
|
|
3
|
+
import type { ButtonOptions } from '@pixi/ui';
|
|
4
4
|
|
|
5
|
-
export type ButtonState = '
|
|
5
|
+
export type ButtonState = 'default' | 'hover' | 'pressed' | 'disabled';
|
|
6
6
|
|
|
7
7
|
export interface ButtonConfig {
|
|
8
8
|
/** Default texture/sprite for each state (optional — uses Graphics if not provided) */
|
|
@@ -21,179 +21,120 @@ export interface ButtonConfig {
|
|
|
21
21
|
animationDuration?: number;
|
|
22
22
|
/** Start disabled */
|
|
23
23
|
disabled?: boolean;
|
|
24
|
+
/** Button text */
|
|
25
|
+
text?: string;
|
|
26
|
+
/** Button text style */
|
|
27
|
+
textStyle?: Record<string, unknown>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
const DEFAULT_COLORS: Record<ButtonState, number> = {
|
|
27
|
-
|
|
31
|
+
default: 0xffd700,
|
|
28
32
|
hover: 0xffe44d,
|
|
29
33
|
pressed: 0xccac00,
|
|
30
34
|
disabled: 0x666666,
|
|
31
35
|
};
|
|
32
36
|
|
|
37
|
+
function makeGraphicsView(
|
|
38
|
+
w: number, h: number, radius: number, color: number,
|
|
39
|
+
): Graphics {
|
|
40
|
+
const g = new Graphics();
|
|
41
|
+
g.roundRect(0, 0, w, h, radius).fill(color);
|
|
42
|
+
// Highlight overlay
|
|
43
|
+
g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
|
|
44
|
+
return g;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
|
-
* Interactive button component
|
|
48
|
+
* Interactive button component powered by `@pixi/ui` FancyButton.
|
|
35
49
|
*
|
|
36
|
-
* Supports both texture-based and Graphics-based rendering
|
|
50
|
+
* Supports both texture-based and Graphics-based rendering with
|
|
51
|
+
* per-state views, press animation, and text.
|
|
37
52
|
*
|
|
38
53
|
* @example
|
|
39
54
|
* ```ts
|
|
40
55
|
* const btn = new Button({
|
|
41
56
|
* width: 200, height: 60, borderRadius: 12,
|
|
42
|
-
* colors: {
|
|
57
|
+
* colors: { default: 0x22aa22, hover: 0x33cc33 },
|
|
58
|
+
* text: 'SPIN',
|
|
43
59
|
* });
|
|
44
60
|
*
|
|
45
|
-
* btn.
|
|
61
|
+
* btn.onPress.connect(() => console.log('Clicked!'));
|
|
46
62
|
* scene.container.addChild(btn);
|
|
47
63
|
* ```
|
|
48
64
|
*/
|
|
49
|
-
export class Button extends
|
|
50
|
-
private
|
|
51
|
-
private _bg: Graphics;
|
|
52
|
-
private _sprites: Partial<Record<ButtonState, Sprite>> = {};
|
|
53
|
-
private _config: Required<
|
|
65
|
+
export class Button extends FancyButton {
|
|
66
|
+
private _buttonConfig: Required<
|
|
54
67
|
Pick<ButtonConfig, 'width' | 'height' | 'borderRadius' | 'pressScale' | 'animationDuration'>
|
|
55
68
|
> & ButtonConfig;
|
|
56
69
|
|
|
57
|
-
/** Called when the button is tapped/clicked */
|
|
58
|
-
public onTap?: () => void;
|
|
59
|
-
|
|
60
|
-
/** Called when the button state changes */
|
|
61
|
-
public onStateChange?: (state: ButtonState) => void;
|
|
62
|
-
|
|
63
70
|
constructor(config: ButtonConfig = {}) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pressScale: 0.95,
|
|
71
|
-
animationDuration: 100,
|
|
71
|
+
const resolvedConfig = {
|
|
72
|
+
width: config.width ?? 200,
|
|
73
|
+
height: config.height ?? 60,
|
|
74
|
+
borderRadius: config.borderRadius ?? 8,
|
|
75
|
+
pressScale: config.pressScale ?? 0.95,
|
|
76
|
+
animationDuration: config.animationDuration ?? 100,
|
|
72
77
|
...config,
|
|
73
78
|
};
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
const colorMap = { ...DEFAULT_COLORS, ...config.colors };
|
|
81
|
+
const { width, height, borderRadius } = resolvedConfig;
|
|
82
|
+
|
|
83
|
+
// Build FancyButton options
|
|
84
|
+
const options: ButtonOptions = {
|
|
85
|
+
anchor: 0.5,
|
|
86
|
+
animations: {
|
|
87
|
+
hover: {
|
|
88
|
+
props: { scale: { x: 1.03, y: 1.03 } },
|
|
89
|
+
duration: resolvedConfig.animationDuration,
|
|
90
|
+
},
|
|
91
|
+
pressed: {
|
|
92
|
+
props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
|
|
93
|
+
duration: resolvedConfig.animationDuration,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
78
97
|
|
|
79
|
-
//
|
|
98
|
+
// Texture-based views
|
|
80
99
|
if (config.textures) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
if (config.textures.default) options.defaultView = config.textures.default as any;
|
|
101
|
+
if (config.textures.hover) options.hoverView = config.textures.hover as any;
|
|
102
|
+
if (config.textures.pressed) options.pressedView = config.textures.pressed as any;
|
|
103
|
+
if (config.textures.disabled) options.disabledView = config.textures.disabled as any;
|
|
104
|
+
} else {
|
|
105
|
+
// Graphics-based views
|
|
106
|
+
options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
|
|
107
|
+
options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
|
|
108
|
+
options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
|
|
109
|
+
options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Set up hit area for Graphics-based
|
|
96
|
-
this.pivot.set(this._config.width / 2, this._config.height / 2);
|
|
112
|
+
// Text
|
|
113
|
+
if (config.text) {
|
|
114
|
+
options.text = config.text;
|
|
115
|
+
}
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
this.on('pointerover', this.onPointerOver);
|
|
100
|
-
this.on('pointerout', this.onPointerOut);
|
|
101
|
-
this.on('pointerdown', this.onPointerDown);
|
|
102
|
-
this.on('pointerup', this.onPointerUp);
|
|
103
|
-
this.on('pointertap', this.onPointerTap);
|
|
117
|
+
super(options);
|
|
104
118
|
|
|
105
|
-
|
|
106
|
-
this.setState('normal');
|
|
119
|
+
this._buttonConfig = resolvedConfig;
|
|
107
120
|
|
|
108
121
|
if (config.disabled) {
|
|
109
|
-
this.
|
|
122
|
+
this.enabled = false;
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
/** Current button state */
|
|
114
|
-
get state(): ButtonState {
|
|
115
|
-
return this._state;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
126
|
/** Enable the button */
|
|
119
127
|
enable(): void {
|
|
120
|
-
|
|
121
|
-
this.setState('normal');
|
|
122
|
-
this.eventMode = 'static';
|
|
123
|
-
this.cursor = 'pointer';
|
|
124
|
-
}
|
|
128
|
+
this.enabled = true;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/** Disable the button */
|
|
128
132
|
disable(): void {
|
|
129
|
-
this.
|
|
130
|
-
this.eventMode = 'none';
|
|
131
|
-
this.cursor = 'default';
|
|
133
|
+
this.enabled = false;
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/** Whether the button is disabled */
|
|
135
137
|
get disabled(): boolean {
|
|
136
|
-
return this.
|
|
138
|
+
return !this.enabled;
|
|
137
139
|
}
|
|
138
|
-
|
|
139
|
-
private setState(state: ButtonState): void {
|
|
140
|
-
if (this._state === state) return;
|
|
141
|
-
this._state = state;
|
|
142
|
-
this.render();
|
|
143
|
-
this.onStateChange?.(state);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private render(): void {
|
|
147
|
-
const { width, height, borderRadius, colors } = this._config;
|
|
148
|
-
const colorMap = { ...DEFAULT_COLORS, ...colors };
|
|
149
|
-
|
|
150
|
-
// Update Graphics
|
|
151
|
-
this._bg.clear();
|
|
152
|
-
this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
|
|
153
|
-
|
|
154
|
-
// Add highlight for normal/hover
|
|
155
|
-
if (this._state === 'normal' || this._state === 'hover') {
|
|
156
|
-
this._bg
|
|
157
|
-
.roundRect(2, 2, width - 4, height * 0.45, borderRadius)
|
|
158
|
-
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Update sprite visibility
|
|
162
|
-
for (const [state, sprite] of Object.entries(this._sprites)) {
|
|
163
|
-
if (sprite) sprite.visible = state === this._state;
|
|
164
|
-
}
|
|
165
|
-
// Fall back to normal sprite if state sprite doesn't exist
|
|
166
|
-
if (!this._sprites[this._state] && this._sprites.normal) {
|
|
167
|
-
this._sprites.normal.visible = true;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private onPointerOver = (): void => {
|
|
172
|
-
if (this._state === 'disabled') return;
|
|
173
|
-
this.setState('hover');
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
private onPointerOut = (): void => {
|
|
177
|
-
if (this._state === 'disabled') return;
|
|
178
|
-
this.setState('normal');
|
|
179
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
private onPointerDown = (): void => {
|
|
183
|
-
if (this._state === 'disabled') return;
|
|
184
|
-
this.setState('pressed');
|
|
185
|
-
const s = this._config.pressScale;
|
|
186
|
-
Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
private onPointerUp = (): void => {
|
|
190
|
-
if (this._state === 'disabled') return;
|
|
191
|
-
this.setState('hover');
|
|
192
|
-
Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
private onPointerTap = (): void => {
|
|
196
|
-
if (this._state === 'disabled') return;
|
|
197
|
-
this.onTap?.();
|
|
198
|
-
};
|
|
199
140
|
}
|
package/src/ui/Layout.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
+
import '@pixi/layout';
|
|
3
|
+
import type { LayoutStyles } from '@pixi/layout';
|
|
2
4
|
|
|
3
5
|
// ─── Types ───────────────────────────────────────────────
|
|
4
6
|
|
|
@@ -38,8 +40,65 @@ export interface LayoutConfig {
|
|
|
38
40
|
breakpoints?: Record<number, Partial<LayoutConfig>>;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const ALIGNMENT_MAP: Record<LayoutAlignment, LayoutStyles['alignItems']> = {
|
|
46
|
+
start: 'flex-start',
|
|
47
|
+
center: 'center',
|
|
48
|
+
end: 'flex-end',
|
|
49
|
+
stretch: 'stretch',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function normalizePadding(
|
|
53
|
+
padding: number | [number, number, number, number],
|
|
54
|
+
): [number, number, number, number] {
|
|
55
|
+
if (typeof padding === 'number') return [padding, padding, padding, padding];
|
|
56
|
+
return padding;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function directionToFlexStyles(
|
|
60
|
+
direction: LayoutDirection,
|
|
61
|
+
maxWidth: number,
|
|
62
|
+
): Partial<LayoutStyles> {
|
|
63
|
+
switch (direction) {
|
|
64
|
+
case 'horizontal':
|
|
65
|
+
return { flexDirection: 'row', flexWrap: 'nowrap' };
|
|
66
|
+
case 'vertical':
|
|
67
|
+
return { flexDirection: 'column', flexWrap: 'nowrap' };
|
|
68
|
+
case 'grid':
|
|
69
|
+
return { flexDirection: 'row', flexWrap: 'wrap' };
|
|
70
|
+
case 'wrap':
|
|
71
|
+
return {
|
|
72
|
+
flexDirection: 'row',
|
|
73
|
+
flexWrap: 'wrap',
|
|
74
|
+
...(maxWidth < Infinity ? { maxWidth } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildLayoutStyles(config: {
|
|
80
|
+
direction: LayoutDirection;
|
|
81
|
+
gap: number;
|
|
82
|
+
alignment: LayoutAlignment;
|
|
83
|
+
columns: number;
|
|
84
|
+
padding: [number, number, number, number];
|
|
85
|
+
maxWidth: number;
|
|
86
|
+
}): LayoutStyles {
|
|
87
|
+
const [pt, pr, pb, pl] = config.padding;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...directionToFlexStyles(config.direction, config.maxWidth),
|
|
91
|
+
gap: config.gap,
|
|
92
|
+
alignItems: ALIGNMENT_MAP[config.alignment],
|
|
93
|
+
paddingTop: pt,
|
|
94
|
+
paddingRight: pr,
|
|
95
|
+
paddingBottom: pb,
|
|
96
|
+
paddingLeft: pl,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
41
100
|
/**
|
|
42
|
-
* Responsive layout container
|
|
101
|
+
* Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
|
|
43
102
|
*
|
|
44
103
|
* Supports horizontal, vertical, grid, and wrap layout modes with
|
|
45
104
|
* alignment, padding, gap, and viewport-anchor positioning.
|
|
@@ -62,17 +121,15 @@ export interface LayoutConfig {
|
|
|
62
121
|
* toolbar.addItem(betLabel);
|
|
63
122
|
* scene.container.addChild(toolbar);
|
|
64
123
|
*
|
|
65
|
-
* // On resize, update layout position relative to viewport
|
|
66
124
|
* toolbar.updateViewport(width, height);
|
|
67
125
|
* ```
|
|
68
126
|
*/
|
|
69
127
|
export class Layout extends Container {
|
|
70
|
-
private
|
|
128
|
+
private _layoutConfig: Required<Pick<LayoutConfig, 'direction' | 'gap' | 'alignment' | 'autoLayout' | 'columns'>>;
|
|
71
129
|
private _padding: [number, number, number, number];
|
|
72
130
|
private _anchor: LayoutAnchor;
|
|
73
131
|
private _maxWidth: number;
|
|
74
132
|
private _breakpoints: [number, Partial<LayoutConfig>][];
|
|
75
|
-
private _content: Container;
|
|
76
133
|
private _items: Container[] = [];
|
|
77
134
|
private _viewportWidth = 0;
|
|
78
135
|
private _viewportHeight = 0;
|
|
@@ -80,7 +137,7 @@ export class Layout extends Container {
|
|
|
80
137
|
constructor(config: LayoutConfig = {}) {
|
|
81
138
|
super();
|
|
82
139
|
|
|
83
|
-
this.
|
|
140
|
+
this._layoutConfig = {
|
|
84
141
|
direction: config.direction ?? 'vertical',
|
|
85
142
|
gap: config.gap ?? 0,
|
|
86
143
|
alignment: config.alignment ?? 'start',
|
|
@@ -88,26 +145,28 @@ export class Layout extends Container {
|
|
|
88
145
|
columns: config.columns ?? 2,
|
|
89
146
|
};
|
|
90
147
|
|
|
91
|
-
this._padding =
|
|
148
|
+
this._padding = normalizePadding(config.padding ?? 0);
|
|
92
149
|
this._anchor = config.anchor ?? 'top-left';
|
|
93
150
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
94
151
|
|
|
95
|
-
// Sort breakpoints by width ascending for correct resolution
|
|
96
152
|
this._breakpoints = config.breakpoints
|
|
97
153
|
? Object.entries(config.breakpoints)
|
|
98
154
|
.map(([w, cfg]) => [Number(w), cfg] as [number, Partial<LayoutConfig>])
|
|
99
155
|
.sort((a, b) => a[0] - b[0])
|
|
100
156
|
: [];
|
|
101
157
|
|
|
102
|
-
this.
|
|
103
|
-
this.addChild(this._content);
|
|
158
|
+
this.applyLayoutStyles();
|
|
104
159
|
}
|
|
105
160
|
|
|
106
161
|
/** Add an item to the layout */
|
|
107
162
|
addItem(child: Container): this {
|
|
108
163
|
this._items.push(child);
|
|
109
|
-
this.
|
|
110
|
-
|
|
164
|
+
this.addChild(child);
|
|
165
|
+
|
|
166
|
+
if (this._layoutConfig.direction === 'grid') {
|
|
167
|
+
this.applyGridChildWidth(child);
|
|
168
|
+
}
|
|
169
|
+
|
|
111
170
|
return this;
|
|
112
171
|
}
|
|
113
172
|
|
|
@@ -116,8 +175,7 @@ export class Layout extends Container {
|
|
|
116
175
|
const idx = this._items.indexOf(child);
|
|
117
176
|
if (idx !== -1) {
|
|
118
177
|
this._items.splice(idx, 1);
|
|
119
|
-
this.
|
|
120
|
-
if (this._config.autoLayout) this.layout();
|
|
178
|
+
this.removeChild(child);
|
|
121
179
|
}
|
|
122
180
|
return this;
|
|
123
181
|
}
|
|
@@ -125,10 +183,9 @@ export class Layout extends Container {
|
|
|
125
183
|
/** Remove all items */
|
|
126
184
|
clearItems(): this {
|
|
127
185
|
for (const item of this._items) {
|
|
128
|
-
this.
|
|
186
|
+
this.removeChild(item);
|
|
129
187
|
}
|
|
130
188
|
this._items.length = 0;
|
|
131
|
-
if (this._config.autoLayout) this.layout();
|
|
132
189
|
return this;
|
|
133
190
|
}
|
|
134
191
|
|
|
@@ -144,192 +201,74 @@ export class Layout extends Container {
|
|
|
144
201
|
updateViewport(width: number, height: number): void {
|
|
145
202
|
this._viewportWidth = width;
|
|
146
203
|
this._viewportHeight = height;
|
|
147
|
-
this.
|
|
204
|
+
this.applyLayoutStyles();
|
|
205
|
+
this.applyAnchor();
|
|
148
206
|
}
|
|
149
207
|
|
|
150
|
-
|
|
151
|
-
* Recalculate layout positions of all children.
|
|
152
|
-
*/
|
|
153
|
-
layout(): void {
|
|
154
|
-
if (this._items.length === 0) return;
|
|
155
|
-
|
|
156
|
-
// Resolve effective config (apply breakpoint overrides)
|
|
208
|
+
private applyLayoutStyles(): void {
|
|
157
209
|
const effective = this.resolveConfig();
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const alignment = effective.alignment ?? this.
|
|
161
|
-
const columns = effective.columns ?? this.
|
|
210
|
+
const direction = effective.direction ?? this._layoutConfig.direction;
|
|
211
|
+
const gap = effective.gap ?? this._layoutConfig.gap;
|
|
212
|
+
const alignment = effective.alignment ?? this._layoutConfig.alignment;
|
|
213
|
+
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
162
214
|
const padding = effective.padding !== undefined
|
|
163
|
-
?
|
|
215
|
+
? normalizePadding(effective.padding)
|
|
164
216
|
: this._padding;
|
|
165
217
|
const maxWidth = effective.maxWidth ?? this._maxWidth;
|
|
166
218
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
switch (direction) {
|
|
170
|
-
case 'horizontal':
|
|
171
|
-
this.layoutLinear('x', 'y', gap, alignment, pl, pt);
|
|
172
|
-
break;
|
|
173
|
-
case 'vertical':
|
|
174
|
-
this.layoutLinear('y', 'x', gap, alignment, pt, pl);
|
|
175
|
-
break;
|
|
176
|
-
case 'grid':
|
|
177
|
-
this.layoutGrid(columns, gap, alignment, pl, pt);
|
|
178
|
-
break;
|
|
179
|
-
case 'wrap':
|
|
180
|
-
this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
219
|
+
const styles = buildLayoutStyles({ direction, gap, alignment, columns, padding, maxWidth });
|
|
220
|
+
this.layout = styles;
|
|
183
221
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// ─── Private layout helpers ────────────────────────────
|
|
189
|
-
|
|
190
|
-
private layoutLinear(
|
|
191
|
-
mainAxis: 'x' | 'y',
|
|
192
|
-
crossAxis: 'x' | 'y',
|
|
193
|
-
gap: number,
|
|
194
|
-
alignment: LayoutAlignment,
|
|
195
|
-
mainOffset: number,
|
|
196
|
-
crossOffset: number,
|
|
197
|
-
): void {
|
|
198
|
-
let pos = mainOffset;
|
|
199
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
200
|
-
const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
|
|
201
|
-
|
|
202
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
203
|
-
const item = this._items[i];
|
|
204
|
-
const size = sizes[i];
|
|
205
|
-
|
|
206
|
-
item[mainAxis] = pos;
|
|
207
|
-
|
|
208
|
-
// Cross-axis alignment
|
|
209
|
-
const itemCross = crossAxis === 'x' ? size.width : size.height;
|
|
210
|
-
switch (alignment) {
|
|
211
|
-
case 'start':
|
|
212
|
-
item[crossAxis] = crossOffset;
|
|
213
|
-
break;
|
|
214
|
-
case 'center':
|
|
215
|
-
item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
|
|
216
|
-
break;
|
|
217
|
-
case 'end':
|
|
218
|
-
item[crossAxis] = crossOffset + maxCross - itemCross;
|
|
219
|
-
break;
|
|
220
|
-
case 'stretch':
|
|
221
|
-
item[crossAxis] = crossOffset;
|
|
222
|
-
// Note: stretch doesn't resize children — that's up to the item
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const mainSize = mainAxis === 'x' ? size.width : size.height;
|
|
227
|
-
pos += mainSize + gap;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private layoutGrid(
|
|
232
|
-
columns: number,
|
|
233
|
-
gap: number,
|
|
234
|
-
alignment: LayoutAlignment,
|
|
235
|
-
offsetX: number,
|
|
236
|
-
offsetY: number,
|
|
237
|
-
): void {
|
|
238
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
239
|
-
const maxItemWidth = Math.max(...sizes.map(s => s.width));
|
|
240
|
-
const maxItemHeight = Math.max(...sizes.map(s => s.height));
|
|
241
|
-
const cellW = maxItemWidth + gap;
|
|
242
|
-
const cellH = maxItemHeight + gap;
|
|
243
|
-
|
|
244
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
245
|
-
const item = this._items[i];
|
|
246
|
-
const col = i % columns;
|
|
247
|
-
const row = Math.floor(i / columns);
|
|
248
|
-
const size = sizes[i];
|
|
249
|
-
|
|
250
|
-
// X alignment within cell
|
|
251
|
-
switch (alignment) {
|
|
252
|
-
case 'center':
|
|
253
|
-
item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
|
|
254
|
-
break;
|
|
255
|
-
case 'end':
|
|
256
|
-
item.x = offsetX + col * cellW + maxItemWidth - size.width;
|
|
257
|
-
break;
|
|
258
|
-
default:
|
|
259
|
-
item.x = offsetX + col * cellW;
|
|
222
|
+
if (direction === 'grid') {
|
|
223
|
+
for (const item of this._items) {
|
|
224
|
+
this.applyGridChildWidth(item);
|
|
260
225
|
}
|
|
261
|
-
|
|
262
|
-
item.y = offsetY + row * cellH;
|
|
263
226
|
}
|
|
264
227
|
}
|
|
265
228
|
|
|
266
|
-
private
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
alignment: LayoutAlignment,
|
|
270
|
-
offsetX: number,
|
|
271
|
-
offsetY: number,
|
|
272
|
-
): void {
|
|
273
|
-
let x = offsetX;
|
|
274
|
-
let y = offsetY;
|
|
275
|
-
let rowHeight = 0;
|
|
276
|
-
const sizes = this._items.map(item => this.getItemSize(item));
|
|
277
|
-
|
|
278
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
279
|
-
const item = this._items[i];
|
|
280
|
-
const size = sizes[i];
|
|
281
|
-
|
|
282
|
-
// Check if item fits in current row
|
|
283
|
-
if (x + size.width > maxWidth + offsetX && x > offsetX) {
|
|
284
|
-
// Wrap to next row
|
|
285
|
-
x = offsetX;
|
|
286
|
-
y += rowHeight + gap;
|
|
287
|
-
rowHeight = 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
item.x = x;
|
|
291
|
-
item.y = y;
|
|
229
|
+
private applyGridChildWidth(child: Container): void {
|
|
230
|
+
const effective = this.resolveConfig();
|
|
231
|
+
const columns = effective.columns ?? this._layoutConfig.columns;
|
|
292
232
|
|
|
293
|
-
|
|
294
|
-
|
|
233
|
+
const pct = `${(100 / columns).toFixed(2)}%` as `${number}%`;
|
|
234
|
+
if (child._layout) {
|
|
235
|
+
child._layout.setStyle({ width: pct });
|
|
236
|
+
} else {
|
|
237
|
+
child.layout = { width: pct };
|
|
295
238
|
}
|
|
296
239
|
}
|
|
297
240
|
|
|
298
|
-
private applyAnchor(
|
|
241
|
+
private applyAnchor(): void {
|
|
242
|
+
const anchor = this.resolveConfig().anchor ?? this._anchor;
|
|
299
243
|
if (this._viewportWidth === 0 || this._viewportHeight === 0) return;
|
|
300
244
|
|
|
301
|
-
const bounds = this.
|
|
302
|
-
const contentW = bounds.width;
|
|
303
|
-
const contentH = bounds.height;
|
|
245
|
+
const bounds = this.getLocalBounds();
|
|
246
|
+
const contentW = bounds.width * this.scale.x;
|
|
247
|
+
const contentH = bounds.height * this.scale.y;
|
|
304
248
|
const vw = this._viewportWidth;
|
|
305
249
|
const vh = this._viewportHeight;
|
|
306
250
|
|
|
307
251
|
let anchorX = 0;
|
|
308
252
|
let anchorY = 0;
|
|
309
253
|
|
|
310
|
-
// Horizontal
|
|
311
254
|
if (anchor.includes('left')) {
|
|
312
255
|
anchorX = 0;
|
|
313
256
|
} else if (anchor.includes('right')) {
|
|
314
257
|
anchorX = vw - contentW;
|
|
315
258
|
} else {
|
|
316
|
-
// center
|
|
317
259
|
anchorX = (vw - contentW) / 2;
|
|
318
260
|
}
|
|
319
261
|
|
|
320
|
-
// Vertical
|
|
321
262
|
if (anchor.startsWith('top')) {
|
|
322
263
|
anchorY = 0;
|
|
323
264
|
} else if (anchor.startsWith('bottom')) {
|
|
324
265
|
anchorY = vh - contentH;
|
|
325
266
|
} else {
|
|
326
|
-
// center
|
|
327
267
|
anchorY = (vh - contentH) / 2;
|
|
328
268
|
}
|
|
329
269
|
|
|
330
|
-
|
|
331
|
-
this.
|
|
332
|
-
this.y = anchorY - bounds.y;
|
|
270
|
+
this.x = anchorX - bounds.x * this.scale.x;
|
|
271
|
+
this.y = anchorY - bounds.y * this.scale.y;
|
|
333
272
|
}
|
|
334
273
|
|
|
335
274
|
private resolveConfig(): Partial<LayoutConfig> {
|
|
@@ -337,28 +276,11 @@ export class Layout extends Container {
|
|
|
337
276
|
return {};
|
|
338
277
|
}
|
|
339
278
|
|
|
340
|
-
// Find the largest breakpoint that's ≤ current viewport width
|
|
341
|
-
let resolved: Partial<LayoutConfig> = {};
|
|
342
279
|
for (const [maxWidth, overrides] of this._breakpoints) {
|
|
343
280
|
if (this._viewportWidth <= maxWidth) {
|
|
344
|
-
|
|
345
|
-
break;
|
|
281
|
+
return overrides;
|
|
346
282
|
}
|
|
347
283
|
}
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private getItemSize(item: Container): { width: number; height: number } {
|
|
352
|
-
const bounds = item.getBounds();
|
|
353
|
-
return { width: bounds.width, height: bounds.height };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private static normalizePadding(
|
|
357
|
-
padding: number | [number, number, number, number],
|
|
358
|
-
): [number, number, number, number] {
|
|
359
|
-
if (typeof padding === 'number') {
|
|
360
|
-
return [padding, padding, padding, padding];
|
|
361
|
-
}
|
|
362
|
-
return padding;
|
|
284
|
+
return {};
|
|
363
285
|
}
|
|
364
286
|
}
|
package/src/ui/Modal.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ModalConfig {
|
|
|
17
17
|
* Modal overlay component.
|
|
18
18
|
* Shows content on top of a dark overlay with enter/exit animations.
|
|
19
19
|
*
|
|
20
|
+
* The content container uses `@pixi/layout` for automatic centering.
|
|
21
|
+
*
|
|
20
22
|
* @example
|
|
21
23
|
* ```ts
|
|
22
24
|
* const modal = new Modal({ closeOnOverlay: true });
|
|
@@ -38,11 +40,10 @@ export class Modal extends Container {
|
|
|
38
40
|
super();
|
|
39
41
|
|
|
40
42
|
this._config = {
|
|
41
|
-
overlayColor: 0x000000,
|
|
42
|
-
overlayAlpha: 0.7,
|
|
43
|
-
closeOnOverlay: true,
|
|
44
|
-
animationDuration: 300,
|
|
45
|
-
...config,
|
|
43
|
+
overlayColor: config.overlayColor ?? 0x000000,
|
|
44
|
+
overlayAlpha: config.overlayAlpha ?? 0.7,
|
|
45
|
+
closeOnOverlay: config.closeOnOverlay ?? true,
|
|
46
|
+
animationDuration: config.animationDuration ?? 300,
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
// Overlay
|