@energy8platform/game-engine 0.1.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 +1134 -0
- package/dist/animation.cjs.js +505 -0
- package/dist/animation.cjs.js.map +1 -0
- package/dist/animation.d.ts +235 -0
- package/dist/animation.esm.js +500 -0
- package/dist/animation.esm.js.map +1 -0
- package/dist/assets.cjs.js +148 -0
- package/dist/assets.cjs.js.map +1 -0
- package/dist/assets.d.ts +97 -0
- package/dist/assets.esm.js +146 -0
- package/dist/assets.esm.js.map +1 -0
- package/dist/audio.cjs.js +345 -0
- package/dist/audio.cjs.js.map +1 -0
- package/dist/audio.d.ts +135 -0
- package/dist/audio.esm.js +343 -0
- package/dist/audio.esm.js.map +1 -0
- package/dist/core.cjs.js +1832 -0
- package/dist/core.cjs.js.map +1 -0
- package/dist/core.d.ts +633 -0
- package/dist/core.esm.js +1827 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/debug.cjs.js +298 -0
- package/dist/debug.cjs.js.map +1 -0
- package/dist/debug.d.ts +114 -0
- package/dist/debug.esm.js +295 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.cjs.js +3623 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +1607 -0
- package/dist/index.esm.js +3598 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/ui.cjs.js +1081 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.d.ts +387 -0
- package/dist/ui.esm.js +1072 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vite.cjs.js +125 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.esm.js +122 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +107 -0
- package/src/animation/Easing.ts +116 -0
- package/src/animation/SpineHelper.ts +162 -0
- package/src/animation/Timeline.ts +138 -0
- package/src/animation/Tween.ts +225 -0
- package/src/animation/index.ts +4 -0
- package/src/assets/AssetManager.ts +174 -0
- package/src/assets/index.ts +2 -0
- package/src/audio/AudioManager.ts +366 -0
- package/src/audio/index.ts +1 -0
- package/src/core/EventEmitter.ts +47 -0
- package/src/core/GameApplication.ts +306 -0
- package/src/core/Scene.ts +48 -0
- package/src/core/SceneManager.ts +258 -0
- package/src/core/index.ts +4 -0
- package/src/debug/DevBridge.ts +259 -0
- package/src/debug/FPSOverlay.ts +102 -0
- package/src/debug/index.ts +3 -0
- package/src/index.ts +71 -0
- package/src/input/InputManager.ts +171 -0
- package/src/input/index.ts +1 -0
- package/src/loading/CSSPreloader.ts +155 -0
- package/src/loading/LoadingScene.ts +356 -0
- package/src/loading/index.ts +2 -0
- package/src/state/StateMachine.ts +228 -0
- package/src/state/index.ts +1 -0
- package/src/types.ts +218 -0
- package/src/ui/BalanceDisplay.ts +155 -0
- package/src/ui/Button.ts +199 -0
- package/src/ui/Label.ts +111 -0
- package/src/ui/Modal.ts +134 -0
- package/src/ui/Panel.ts +125 -0
- package/src/ui/ProgressBar.ts +121 -0
- package/src/ui/Toast.ts +124 -0
- package/src/ui/WinDisplay.ts +133 -0
- package/src/ui/index.ts +16 -0
- package/src/viewport/ViewportManager.ts +241 -0
- package/src/viewport/index.ts +1 -0
- package/src/vite/index.ts +153 -0
package/src/ui/Button.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Container, Graphics, Sprite, Texture, FederatedPointerEvent } from 'pixi.js';
|
|
2
|
+
import { Tween } from '../animation/Tween';
|
|
3
|
+
import { Easing } from '../animation/Easing';
|
|
4
|
+
|
|
5
|
+
export type ButtonState = 'normal' | 'hover' | 'pressed' | 'disabled';
|
|
6
|
+
|
|
7
|
+
export interface ButtonConfig {
|
|
8
|
+
/** Default texture/sprite for each state (optional — uses Graphics if not provided) */
|
|
9
|
+
textures?: Partial<Record<ButtonState, string | Texture>>;
|
|
10
|
+
/** Width (for Graphics-based button) */
|
|
11
|
+
width?: number;
|
|
12
|
+
/** Height (for Graphics-based button) */
|
|
13
|
+
height?: number;
|
|
14
|
+
/** Corner radius (for Graphics-based button) */
|
|
15
|
+
borderRadius?: number;
|
|
16
|
+
/** Colors for each state (for Graphics-based button) */
|
|
17
|
+
colors?: Partial<Record<ButtonState, number>>;
|
|
18
|
+
/** Scale on press */
|
|
19
|
+
pressScale?: number;
|
|
20
|
+
/** Scale animation duration (ms) */
|
|
21
|
+
animationDuration?: number;
|
|
22
|
+
/** Start disabled */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_COLORS: Record<ButtonState, number> = {
|
|
27
|
+
normal: 0xffd700,
|
|
28
|
+
hover: 0xffe44d,
|
|
29
|
+
pressed: 0xccac00,
|
|
30
|
+
disabled: 0x666666,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Interactive button component with state management and animation.
|
|
35
|
+
*
|
|
36
|
+
* Supports both texture-based and Graphics-based rendering.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const btn = new Button({
|
|
41
|
+
* width: 200, height: 60, borderRadius: 12,
|
|
42
|
+
* colors: { normal: 0x22aa22, hover: 0x33cc33 },
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* btn.onTap = () => console.log('Clicked!');
|
|
46
|
+
* scene.container.addChild(btn);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export class Button extends Container {
|
|
50
|
+
private _state: ButtonState = 'normal';
|
|
51
|
+
private _bg: Graphics;
|
|
52
|
+
private _sprites: Partial<Record<ButtonState, Sprite>> = {};
|
|
53
|
+
private _config: Required<
|
|
54
|
+
Pick<ButtonConfig, 'width' | 'height' | 'borderRadius' | 'pressScale' | 'animationDuration'>
|
|
55
|
+
> & ButtonConfig;
|
|
56
|
+
|
|
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
|
+
constructor(config: ButtonConfig = {}) {
|
|
64
|
+
super();
|
|
65
|
+
|
|
66
|
+
this._config = {
|
|
67
|
+
width: 200,
|
|
68
|
+
height: 60,
|
|
69
|
+
borderRadius: 8,
|
|
70
|
+
pressScale: 0.95,
|
|
71
|
+
animationDuration: 100,
|
|
72
|
+
...config,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Create Graphics background
|
|
76
|
+
this._bg = new Graphics();
|
|
77
|
+
this.addChild(this._bg);
|
|
78
|
+
|
|
79
|
+
// Create texture sprites if provided
|
|
80
|
+
if (config.textures) {
|
|
81
|
+
for (const [state, tex] of Object.entries(config.textures)) {
|
|
82
|
+
const texture = typeof tex === 'string' ? Texture.from(tex) : tex;
|
|
83
|
+
const sprite = new Sprite(texture);
|
|
84
|
+
sprite.anchor.set(0.5);
|
|
85
|
+
sprite.visible = state === 'normal';
|
|
86
|
+
this._sprites[state as ButtonState] = sprite;
|
|
87
|
+
this.addChild(sprite);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Make interactive
|
|
92
|
+
this.eventMode = 'static';
|
|
93
|
+
this.cursor = 'pointer';
|
|
94
|
+
|
|
95
|
+
// Set up hit area for Graphics-based
|
|
96
|
+
this.pivot.set(this._config.width / 2, this._config.height / 2);
|
|
97
|
+
|
|
98
|
+
// Bind events
|
|
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);
|
|
104
|
+
|
|
105
|
+
// Initial render
|
|
106
|
+
this.setState('normal');
|
|
107
|
+
|
|
108
|
+
if (config.disabled) {
|
|
109
|
+
this.disable();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Current button state */
|
|
114
|
+
get state(): ButtonState {
|
|
115
|
+
return this._state;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Enable the button */
|
|
119
|
+
enable(): void {
|
|
120
|
+
if (this._state === 'disabled') {
|
|
121
|
+
this.setState('normal');
|
|
122
|
+
this.eventMode = 'static';
|
|
123
|
+
this.cursor = 'pointer';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Disable the button */
|
|
128
|
+
disable(): void {
|
|
129
|
+
this.setState('disabled');
|
|
130
|
+
this.eventMode = 'none';
|
|
131
|
+
this.cursor = 'default';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Whether the button is disabled */
|
|
135
|
+
get disabled(): boolean {
|
|
136
|
+
return this._state === 'disabled';
|
|
137
|
+
}
|
|
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
|
+
}
|
package/src/ui/Label.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Container, Text, TextStyle } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
export interface LabelConfig {
|
|
4
|
+
text?: string;
|
|
5
|
+
style?: Partial<TextStyle>;
|
|
6
|
+
/** Maximum width — text will be scaled down to fit */
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
/** Auto-fit: scale text to fit maxWidth */
|
|
9
|
+
autoFit?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enhanced text label with auto-fit scaling and currency formatting.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const label = new Label({
|
|
18
|
+
* text: 'BALANCE',
|
|
19
|
+
* style: { fontSize: 24, fill: 0xffd700 },
|
|
20
|
+
* maxWidth: 200,
|
|
21
|
+
* autoFit: true,
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class Label extends Container {
|
|
26
|
+
private _text: Text;
|
|
27
|
+
private _maxWidth: number;
|
|
28
|
+
private _autoFit: boolean;
|
|
29
|
+
|
|
30
|
+
constructor(config: LabelConfig = {}) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
this._maxWidth = config.maxWidth ?? Infinity;
|
|
34
|
+
this._autoFit = config.autoFit ?? false;
|
|
35
|
+
|
|
36
|
+
this._text = new Text({
|
|
37
|
+
text: config.text ?? '',
|
|
38
|
+
style: {
|
|
39
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
40
|
+
fontSize: 24,
|
|
41
|
+
fill: 0xffffff,
|
|
42
|
+
...config.style,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
this._text.anchor.set(0.5);
|
|
46
|
+
this.addChild(this._text);
|
|
47
|
+
|
|
48
|
+
this.fitText();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get/set the displayed text */
|
|
52
|
+
get text(): string {
|
|
53
|
+
return this._text.text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set text(value: string) {
|
|
57
|
+
this._text.text = value;
|
|
58
|
+
this.fitText();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get/set the text style */
|
|
62
|
+
get style(): TextStyle {
|
|
63
|
+
return this._text.style as TextStyle;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Set max width constraint */
|
|
67
|
+
set maxWidth(value: number) {
|
|
68
|
+
this._maxWidth = value;
|
|
69
|
+
this.fitText();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format and display a number as currency.
|
|
74
|
+
*
|
|
75
|
+
* @param amount - The numeric amount
|
|
76
|
+
* @param currency - Currency code (e.g., 'USD', 'EUR')
|
|
77
|
+
* @param locale - Locale string (default: 'en-US')
|
|
78
|
+
*/
|
|
79
|
+
setCurrency(amount: number, currency: string, locale = 'en-US'): void {
|
|
80
|
+
try {
|
|
81
|
+
this.text = new Intl.NumberFormat(locale, {
|
|
82
|
+
style: 'currency',
|
|
83
|
+
currency,
|
|
84
|
+
minimumFractionDigits: 2,
|
|
85
|
+
maximumFractionDigits: 2,
|
|
86
|
+
}).format(amount);
|
|
87
|
+
} catch {
|
|
88
|
+
this.text = `${amount.toFixed(2)} ${currency}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Format a number with thousands separators.
|
|
94
|
+
*/
|
|
95
|
+
setNumber(value: number, decimals = 0, locale = 'en-US'): void {
|
|
96
|
+
this.text = new Intl.NumberFormat(locale, {
|
|
97
|
+
minimumFractionDigits: decimals,
|
|
98
|
+
maximumFractionDigits: decimals,
|
|
99
|
+
}).format(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fitText(): void {
|
|
103
|
+
if (!this._autoFit || this._maxWidth === Infinity) return;
|
|
104
|
+
|
|
105
|
+
this._text.scale.set(1);
|
|
106
|
+
if (this._text.width > this._maxWidth) {
|
|
107
|
+
const scale = this._maxWidth / this._text.width;
|
|
108
|
+
this._text.scale.set(scale);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/ui/Modal.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Container, Graphics } from 'pixi.js';
|
|
2
|
+
import { Tween } from '../animation/Tween';
|
|
3
|
+
import { Easing } from '../animation/Easing';
|
|
4
|
+
|
|
5
|
+
export interface ModalConfig {
|
|
6
|
+
/** Overlay color */
|
|
7
|
+
overlayColor?: number;
|
|
8
|
+
/** Overlay alpha */
|
|
9
|
+
overlayAlpha?: number;
|
|
10
|
+
/** Close on overlay tap */
|
|
11
|
+
closeOnOverlay?: boolean;
|
|
12
|
+
/** Animation duration */
|
|
13
|
+
animationDuration?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Modal overlay component.
|
|
18
|
+
* Shows content on top of a dark overlay with enter/exit animations.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const modal = new Modal({ closeOnOverlay: true });
|
|
23
|
+
* modal.content.addChild(settingsPanel);
|
|
24
|
+
* modal.onClose = () => console.log('Closed');
|
|
25
|
+
* await modal.show(1920, 1080);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class Modal extends Container {
|
|
29
|
+
private _overlay: Graphics;
|
|
30
|
+
private _contentContainer: Container;
|
|
31
|
+
private _config: Required<ModalConfig>;
|
|
32
|
+
private _showing = false;
|
|
33
|
+
|
|
34
|
+
/** Called when the modal is closed */
|
|
35
|
+
public onClose?: () => void;
|
|
36
|
+
|
|
37
|
+
constructor(config: ModalConfig = {}) {
|
|
38
|
+
super();
|
|
39
|
+
|
|
40
|
+
this._config = {
|
|
41
|
+
overlayColor: 0x000000,
|
|
42
|
+
overlayAlpha: 0.7,
|
|
43
|
+
closeOnOverlay: true,
|
|
44
|
+
animationDuration: 300,
|
|
45
|
+
...config,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Overlay
|
|
49
|
+
this._overlay = new Graphics();
|
|
50
|
+
this._overlay.eventMode = 'static';
|
|
51
|
+
this.addChild(this._overlay);
|
|
52
|
+
|
|
53
|
+
if (this._config.closeOnOverlay) {
|
|
54
|
+
this._overlay.on('pointertap', () => this.hide());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Content container
|
|
58
|
+
this._contentContainer = new Container();
|
|
59
|
+
this.addChild(this._contentContainer);
|
|
60
|
+
|
|
61
|
+
this.visible = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Content container — add your UI here */
|
|
65
|
+
get content(): Container {
|
|
66
|
+
return this._contentContainer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether the modal is currently showing */
|
|
70
|
+
get isShowing(): boolean {
|
|
71
|
+
return this._showing;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show the modal with animation.
|
|
76
|
+
*/
|
|
77
|
+
async show(viewWidth: number, viewHeight: number): Promise<void> {
|
|
78
|
+
this._showing = true;
|
|
79
|
+
this.visible = true;
|
|
80
|
+
|
|
81
|
+
// Draw overlay to cover full screen
|
|
82
|
+
this._overlay.clear();
|
|
83
|
+
this._overlay.rect(0, 0, viewWidth, viewHeight).fill(this._config.overlayColor);
|
|
84
|
+
this._overlay.alpha = 0;
|
|
85
|
+
|
|
86
|
+
// Center content
|
|
87
|
+
this._contentContainer.x = viewWidth / 2;
|
|
88
|
+
this._contentContainer.y = viewHeight / 2;
|
|
89
|
+
this._contentContainer.alpha = 0;
|
|
90
|
+
this._contentContainer.scale.set(0.8);
|
|
91
|
+
|
|
92
|
+
// Animate in
|
|
93
|
+
await Promise.all([
|
|
94
|
+
Tween.to(
|
|
95
|
+
this._overlay,
|
|
96
|
+
{ alpha: this._config.overlayAlpha },
|
|
97
|
+
this._config.animationDuration,
|
|
98
|
+
Easing.easeOutCubic,
|
|
99
|
+
),
|
|
100
|
+
Tween.to(
|
|
101
|
+
this._contentContainer,
|
|
102
|
+
{ alpha: 1, 'scale.x': 1, 'scale.y': 1 },
|
|
103
|
+
this._config.animationDuration,
|
|
104
|
+
Easing.easeOutBack,
|
|
105
|
+
),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Hide the modal with animation.
|
|
111
|
+
*/
|
|
112
|
+
async hide(): Promise<void> {
|
|
113
|
+
if (!this._showing) return;
|
|
114
|
+
|
|
115
|
+
await Promise.all([
|
|
116
|
+
Tween.to(
|
|
117
|
+
this._overlay,
|
|
118
|
+
{ alpha: 0 },
|
|
119
|
+
this._config.animationDuration * 0.7,
|
|
120
|
+
Easing.easeInCubic,
|
|
121
|
+
),
|
|
122
|
+
Tween.to(
|
|
123
|
+
this._contentContainer,
|
|
124
|
+
{ alpha: 0, 'scale.x': 0.8, 'scale.y': 0.8 },
|
|
125
|
+
this._config.animationDuration * 0.7,
|
|
126
|
+
Easing.easeInCubic,
|
|
127
|
+
),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
this.visible = false;
|
|
131
|
+
this._showing = false;
|
|
132
|
+
this.onClose?.();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/ui/Panel.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Container, Graphics, NineSliceSprite, Texture } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
export interface PanelConfig {
|
|
4
|
+
/** Width */
|
|
5
|
+
width?: number;
|
|
6
|
+
/** Height */
|
|
7
|
+
height?: number;
|
|
8
|
+
/** Background color (for Graphics-based panel) */
|
|
9
|
+
backgroundColor?: number;
|
|
10
|
+
/** Background alpha */
|
|
11
|
+
backgroundAlpha?: number;
|
|
12
|
+
/** Corner radius */
|
|
13
|
+
borderRadius?: number;
|
|
14
|
+
/** Border color */
|
|
15
|
+
borderColor?: number;
|
|
16
|
+
/** Border width */
|
|
17
|
+
borderWidth?: number;
|
|
18
|
+
/** 9-slice texture (if provided, uses NineSliceSprite instead of Graphics) */
|
|
19
|
+
nineSliceTexture?: string | Texture;
|
|
20
|
+
/** 9-slice borders [left, top, right, bottom] */
|
|
21
|
+
nineSliceBorders?: [number, number, number, number];
|
|
22
|
+
/** Padding inside the panel */
|
|
23
|
+
padding?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Background panel that can use either Graphics or 9-slice sprite.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Simple colored panel
|
|
32
|
+
* const panel = new Panel({ width: 400, height: 300, backgroundColor: 0x222222, borderRadius: 12 });
|
|
33
|
+
*
|
|
34
|
+
* // 9-slice panel (texture-based)
|
|
35
|
+
* const panel = new Panel({
|
|
36
|
+
* nineSliceTexture: 'panel-bg',
|
|
37
|
+
* nineSliceBorders: [20, 20, 20, 20],
|
|
38
|
+
* width: 400, height: 300,
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export class Panel extends Container {
|
|
43
|
+
private _bg: Graphics | NineSliceSprite;
|
|
44
|
+
private _content: Container;
|
|
45
|
+
private _config: Required<
|
|
46
|
+
Pick<PanelConfig, 'width' | 'height' | 'padding' | 'backgroundAlpha'>
|
|
47
|
+
> & PanelConfig;
|
|
48
|
+
|
|
49
|
+
constructor(config: PanelConfig = {}) {
|
|
50
|
+
super();
|
|
51
|
+
|
|
52
|
+
this._config = {
|
|
53
|
+
width: 400,
|
|
54
|
+
height: 300,
|
|
55
|
+
padding: 16,
|
|
56
|
+
backgroundAlpha: 1,
|
|
57
|
+
...config,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Create background
|
|
61
|
+
if (config.nineSliceTexture) {
|
|
62
|
+
const texture =
|
|
63
|
+
typeof config.nineSliceTexture === 'string'
|
|
64
|
+
? Texture.from(config.nineSliceTexture)
|
|
65
|
+
: config.nineSliceTexture;
|
|
66
|
+
|
|
67
|
+
const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
|
|
68
|
+
|
|
69
|
+
this._bg = new NineSliceSprite({
|
|
70
|
+
texture,
|
|
71
|
+
leftWidth: left,
|
|
72
|
+
topHeight: top,
|
|
73
|
+
rightWidth: right,
|
|
74
|
+
bottomHeight: bottom,
|
|
75
|
+
});
|
|
76
|
+
(this._bg as NineSliceSprite).width = this._config.width;
|
|
77
|
+
(this._bg as NineSliceSprite).height = this._config.height;
|
|
78
|
+
} else {
|
|
79
|
+
this._bg = new Graphics();
|
|
80
|
+
this.drawGraphicsBg();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this._bg.alpha = this._config.backgroundAlpha;
|
|
84
|
+
this.addChild(this._bg);
|
|
85
|
+
|
|
86
|
+
// Content container with padding
|
|
87
|
+
this._content = new Container();
|
|
88
|
+
this._content.x = this._config.padding;
|
|
89
|
+
this._content.y = this._config.padding;
|
|
90
|
+
this.addChild(this._content);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Content container — add children here */
|
|
94
|
+
get content(): Container {
|
|
95
|
+
return this._content;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Resize the panel */
|
|
99
|
+
setSize(width: number, height: number): void {
|
|
100
|
+
this._config.width = width;
|
|
101
|
+
this._config.height = height;
|
|
102
|
+
|
|
103
|
+
if (this._bg instanceof Graphics) {
|
|
104
|
+
this.drawGraphicsBg();
|
|
105
|
+
} else {
|
|
106
|
+
this._bg.width = width;
|
|
107
|
+
this._bg.height = height;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private drawGraphicsBg(): void {
|
|
112
|
+
const bg = this._bg as Graphics;
|
|
113
|
+
const {
|
|
114
|
+
width, height, backgroundColor, borderRadius, borderColor, borderWidth,
|
|
115
|
+
} = this._config;
|
|
116
|
+
|
|
117
|
+
bg.clear();
|
|
118
|
+
bg.roundRect(0, 0, width!, height!, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
|
|
119
|
+
|
|
120
|
+
if (borderColor !== undefined && borderWidth) {
|
|
121
|
+
bg.roundRect(0, 0, width!, height!, borderRadius ?? 0)
|
|
122
|
+
.stroke({ color: borderColor, width: borderWidth });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Container, Graphics } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
export interface ProgressBarConfig {
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
borderRadius?: number;
|
|
7
|
+
fillColor?: number;
|
|
8
|
+
trackColor?: number;
|
|
9
|
+
borderColor?: number;
|
|
10
|
+
borderWidth?: number;
|
|
11
|
+
/** Animated fill (smoothly interpolate) */
|
|
12
|
+
animated?: boolean;
|
|
13
|
+
/** Animation speed (0..1 per frame, default: 0.1) */
|
|
14
|
+
animationSpeed?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Horizontal progress bar with optional smooth fill animation.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
|
|
23
|
+
* scene.container.addChild(bar);
|
|
24
|
+
* bar.progress = 0.5; // 50%
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class ProgressBar extends Container {
|
|
28
|
+
private _track: Graphics;
|
|
29
|
+
private _fill: Graphics;
|
|
30
|
+
private _border: Graphics;
|
|
31
|
+
private _config: Required<ProgressBarConfig>;
|
|
32
|
+
private _progress = 0;
|
|
33
|
+
private _displayedProgress = 0;
|
|
34
|
+
|
|
35
|
+
constructor(config: ProgressBarConfig = {}) {
|
|
36
|
+
super();
|
|
37
|
+
|
|
38
|
+
this._config = {
|
|
39
|
+
width: 300,
|
|
40
|
+
height: 16,
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
fillColor: 0xffd700,
|
|
43
|
+
trackColor: 0x333333,
|
|
44
|
+
borderColor: 0x555555,
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
animated: true,
|
|
47
|
+
animationSpeed: 0.1,
|
|
48
|
+
...config,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this._track = new Graphics();
|
|
52
|
+
this._fill = new Graphics();
|
|
53
|
+
this._border = new Graphics();
|
|
54
|
+
|
|
55
|
+
this.addChild(this._track, this._fill, this._border);
|
|
56
|
+
this.drawTrack();
|
|
57
|
+
this.drawBorder();
|
|
58
|
+
this.drawFill(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get/set progress (0..1) */
|
|
62
|
+
get progress(): number {
|
|
63
|
+
return this._progress;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
set progress(value: number) {
|
|
67
|
+
this._progress = Math.max(0, Math.min(1, value));
|
|
68
|
+
if (!this._config.animated) {
|
|
69
|
+
this._displayedProgress = this._progress;
|
|
70
|
+
this.drawFill(this._displayedProgress);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Call each frame if animated is true.
|
|
76
|
+
*/
|
|
77
|
+
update(dt: number): void {
|
|
78
|
+
if (!this._config.animated) return;
|
|
79
|
+
if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
|
|
80
|
+
this._displayedProgress = this._progress;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this._displayedProgress +=
|
|
85
|
+
(this._progress - this._displayedProgress) * this._config.animationSpeed;
|
|
86
|
+
this.drawFill(this._displayedProgress);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private drawTrack(): void {
|
|
90
|
+
const { width, height, borderRadius, trackColor } = this._config;
|
|
91
|
+
this._track.clear();
|
|
92
|
+
this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private drawBorder(): void {
|
|
96
|
+
const { width, height, borderRadius, borderColor, borderWidth } = this._config;
|
|
97
|
+
this._border.clear();
|
|
98
|
+
this._border
|
|
99
|
+
.roundRect(0, 0, width, height, borderRadius)
|
|
100
|
+
.stroke({ color: borderColor, width: borderWidth });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private drawFill(progress: number): void {
|
|
104
|
+
const { width, height, borderRadius, fillColor, borderWidth } = this._config;
|
|
105
|
+
const innerWidth = width - borderWidth * 2;
|
|
106
|
+
const innerHeight = height - borderWidth * 2;
|
|
107
|
+
const fillWidth = Math.max(0, innerWidth * progress);
|
|
108
|
+
|
|
109
|
+
this._fill.clear();
|
|
110
|
+
if (fillWidth > 0) {
|
|
111
|
+
this._fill.x = borderWidth;
|
|
112
|
+
this._fill.y = borderWidth;
|
|
113
|
+
this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
|
|
114
|
+
|
|
115
|
+
// Highlight
|
|
116
|
+
this._fill
|
|
117
|
+
.roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
|
|
118
|
+
.fill({ color: 0xffffff, alpha: 0.15 });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|