@energy8platform/game-engine 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -44
- package/dist/core.cjs.js +1 -0
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +1 -0
- package/dist/core.esm.js.map +1 -1
- package/dist/index.cjs.js +296 -787
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +56 -129
- package/dist/index.esm.js +297 -788
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +613 -1104
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +55 -128
- package/dist/ui.esm.js +614 -1105
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +23 -3
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +23 -3
- package/dist/vite.esm.js.map +1 -1
- package/package.json +17 -2
- package/src/core/GameApplication.ts +1 -0
- package/src/index.ts +11 -0
- package/src/ui/BalanceDisplay.ts +0 -3
- package/src/ui/Button.ts +71 -130
- package/src/ui/Layout.ts +109 -181
- package/src/ui/Modal.ts +6 -5
- package/src/ui/Panel.ts +52 -55
- package/src/ui/ProgressBar.ts +52 -57
- package/src/ui/ScrollContainer.ts +58 -489
- package/src/ui/Toast.ts +5 -9
- package/src/ui/index.ts +13 -0
- package/src/vite/index.ts +23 -3
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Container,
|
|
1
|
+
import { Container, type ColorSource } from 'pixi.js';
|
|
2
|
+
import { ScrollBox as PixiScrollBox } from '@pixi/ui';
|
|
3
|
+
import type { ScrollBoxOptions } from '@pixi/ui';
|
|
2
4
|
|
|
3
5
|
// ─── Types ───────────────────────────────────────────────
|
|
4
6
|
|
|
@@ -14,42 +16,39 @@ export interface ScrollContainerConfig {
|
|
|
14
16
|
/** Scroll direction (default: 'vertical') */
|
|
15
17
|
direction?: ScrollDirection;
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** Scrollbar width in pixels (default: 6) */
|
|
21
|
-
scrollbarWidth?: number;
|
|
22
|
-
|
|
23
|
-
/** Scrollbar color (default: 0xffffff) */
|
|
24
|
-
scrollbarColor?: number;
|
|
25
|
-
|
|
26
|
-
/** Scrollbar opacity (default: 0.4) */
|
|
27
|
-
scrollbarAlpha?: number;
|
|
19
|
+
/** Background color (undefined = transparent) */
|
|
20
|
+
backgroundColor?: ColorSource;
|
|
28
21
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
22
|
+
/** Border radius for the mask (default: 0) */
|
|
23
|
+
borderRadius?: number;
|
|
31
24
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
25
|
+
/** Gap between items (default: 0) */
|
|
26
|
+
elementsMargin?: number;
|
|
34
27
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
28
|
+
/** Padding */
|
|
29
|
+
padding?: number;
|
|
37
30
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
31
|
+
/** Disable dynamic rendering (render all items even when offscreen) */
|
|
32
|
+
disableDynamicRendering?: boolean;
|
|
40
33
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
34
|
+
/** Disable easing/inertia */
|
|
35
|
+
disableEasing?: boolean;
|
|
43
36
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
37
|
+
/** Global scroll — scroll even when mouse is not over the component */
|
|
38
|
+
globalScroll?: boolean;
|
|
46
39
|
}
|
|
47
40
|
|
|
41
|
+
const DIRECTION_MAP: Record<ScrollDirection, 'vertical' | 'horizontal' | 'bidirectional'> = {
|
|
42
|
+
vertical: 'vertical',
|
|
43
|
+
horizontal: 'horizontal',
|
|
44
|
+
both: 'bidirectional',
|
|
45
|
+
};
|
|
46
|
+
|
|
48
47
|
/**
|
|
49
|
-
* Scrollable container
|
|
48
|
+
* Scrollable container powered by `@pixi/ui` ScrollBox.
|
|
50
49
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Provides touch/drag scrolling, mouse wheel support, inertia, and
|
|
51
|
+
* dynamic rendering optimization for off-screen items.
|
|
53
52
|
*
|
|
54
53
|
* @example
|
|
55
54
|
* ```ts
|
|
@@ -57,501 +56,71 @@ export interface ScrollContainerConfig {
|
|
|
57
56
|
* width: 600,
|
|
58
57
|
* height: 400,
|
|
59
58
|
* direction: 'vertical',
|
|
60
|
-
*
|
|
61
|
-
* elasticity: 0.3,
|
|
59
|
+
* elementsMargin: 8,
|
|
62
60
|
* });
|
|
63
61
|
*
|
|
64
|
-
* // Add content taller than 400px
|
|
65
|
-
* const list = new Container();
|
|
66
62
|
* for (let i = 0; i < 50; i++) {
|
|
67
|
-
*
|
|
68
|
-
* row.y = i * 40;
|
|
69
|
-
* list.addChild(row);
|
|
63
|
+
* scroll.addItem(createRow(i));
|
|
70
64
|
* }
|
|
71
|
-
* scroll.setContent(list);
|
|
72
65
|
*
|
|
73
66
|
* scene.container.addChild(scroll);
|
|
74
67
|
* ```
|
|
75
68
|
*/
|
|
76
|
-
export class ScrollContainer extends
|
|
77
|
-
private
|
|
78
|
-
Pick<ScrollContainerConfig, 'width' | 'height' | 'direction' | 'showScrollbar' |
|
|
79
|
-
'scrollbarWidth' | 'scrollbarColor' | 'scrollbarAlpha' | 'elasticity' | 'inertia' |
|
|
80
|
-
'snapSize' | 'borderRadius'>
|
|
81
|
-
>;
|
|
82
|
-
|
|
83
|
-
private _viewport: Container;
|
|
84
|
-
private _content: Container | null = null;
|
|
85
|
-
private _mask: Graphics;
|
|
86
|
-
private _bg: Graphics;
|
|
87
|
-
private _scrollbarV: Graphics | null = null;
|
|
88
|
-
private _scrollbarH: Graphics | null = null;
|
|
89
|
-
private _scrollbarFadeTimeout: number | null = null;
|
|
90
|
-
|
|
91
|
-
// Scroll state
|
|
92
|
-
private _scrollX = 0;
|
|
93
|
-
private _scrollY = 0;
|
|
94
|
-
private _velocityX = 0;
|
|
95
|
-
private _velocityY = 0;
|
|
96
|
-
private _isDragging = false;
|
|
97
|
-
private _dragStart = { x: 0, y: 0 };
|
|
98
|
-
private _scrollStart = { x: 0, y: 0 };
|
|
99
|
-
private _lastDragPos = { x: 0, y: 0 };
|
|
100
|
-
private _lastDragTime = 0;
|
|
101
|
-
private _isAnimating = false;
|
|
102
|
-
private _animationFrame: number | null = null;
|
|
69
|
+
export class ScrollContainer extends PixiScrollBox {
|
|
70
|
+
private _scrollConfig: ScrollContainerConfig;
|
|
103
71
|
|
|
104
72
|
constructor(config: ScrollContainerConfig) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this._config = {
|
|
73
|
+
const options: ScrollBoxOptions = {
|
|
108
74
|
width: config.width,
|
|
109
75
|
height: config.height,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
snapSize: config.snapSize ?? 0,
|
|
118
|
-
borderRadius: config.borderRadius ?? 0,
|
|
76
|
+
type: DIRECTION_MAP[config.direction ?? 'vertical'],
|
|
77
|
+
radius: config.borderRadius ?? 0,
|
|
78
|
+
elementsMargin: config.elementsMargin ?? 0,
|
|
79
|
+
padding: config.padding ?? 0,
|
|
80
|
+
disableDynamicRendering: config.disableDynamicRendering ?? false,
|
|
81
|
+
disableEasing: config.disableEasing ?? false,
|
|
82
|
+
globalScroll: config.globalScroll ?? true,
|
|
119
83
|
};
|
|
120
84
|
|
|
121
|
-
// Background
|
|
122
|
-
this._bg = new Graphics();
|
|
123
85
|
if (config.backgroundColor !== undefined) {
|
|
124
|
-
|
|
125
|
-
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
86
|
+
options.background = config.backgroundColor;
|
|
126
87
|
}
|
|
127
|
-
this.addChild(this._bg);
|
|
128
88
|
|
|
129
|
-
|
|
130
|
-
this._viewport = new Container();
|
|
131
|
-
this.addChild(this._viewport);
|
|
89
|
+
super(options);
|
|
132
90
|
|
|
133
|
-
|
|
134
|
-
this._mask = new Graphics();
|
|
135
|
-
this._mask.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
136
|
-
.fill(0xffffff);
|
|
137
|
-
this.addChild(this._mask);
|
|
138
|
-
this._viewport.mask = this._mask;
|
|
139
|
-
|
|
140
|
-
// Scrollbars
|
|
141
|
-
if (this._config.showScrollbar) {
|
|
142
|
-
if (this._config.direction !== 'horizontal') {
|
|
143
|
-
this._scrollbarV = new Graphics();
|
|
144
|
-
this._scrollbarV.alpha = 0;
|
|
145
|
-
this.addChild(this._scrollbarV);
|
|
146
|
-
}
|
|
147
|
-
if (this._config.direction !== 'vertical') {
|
|
148
|
-
this._scrollbarH = new Graphics();
|
|
149
|
-
this._scrollbarH.alpha = 0;
|
|
150
|
-
this.addChild(this._scrollbarH);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Interaction
|
|
155
|
-
this.eventMode = 'static';
|
|
156
|
-
this.cursor = 'grab';
|
|
157
|
-
this.hitArea = { contains: (x: number, y: number) =>
|
|
158
|
-
x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
159
|
-
|
|
160
|
-
this.on('pointerdown', this.onPointerDown);
|
|
161
|
-
this.on('pointermove', this.onPointerMove);
|
|
162
|
-
this.on('pointerup', this.onPointerUp);
|
|
163
|
-
this.on('pointerupoutside', this.onPointerUp);
|
|
164
|
-
this.on('wheel', this.onWheel);
|
|
91
|
+
this._scrollConfig = config;
|
|
165
92
|
}
|
|
166
93
|
|
|
167
94
|
/** Set scrollable content. Replaces any existing content. */
|
|
168
95
|
setContent(content: Container): void {
|
|
169
|
-
|
|
170
|
-
|
|
96
|
+
// Remove existing items
|
|
97
|
+
const existing = this.items;
|
|
98
|
+
if (existing.length > 0) {
|
|
99
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
100
|
+
this.removeItem(i);
|
|
101
|
+
}
|
|
171
102
|
}
|
|
172
|
-
this._content = content;
|
|
173
|
-
this._viewport.addChild(content);
|
|
174
|
-
this._scrollX = 0;
|
|
175
|
-
this._scrollY = 0;
|
|
176
|
-
this.applyScroll();
|
|
177
|
-
}
|
|
178
103
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
/** Scroll to a specific position (in content coordinates) */
|
|
185
|
-
scrollTo(x: number, y: number, animate = true): void {
|
|
186
|
-
if (!animate) {
|
|
187
|
-
this._scrollX = x;
|
|
188
|
-
this._scrollY = y;
|
|
189
|
-
this.clampScroll();
|
|
190
|
-
this.applyScroll();
|
|
191
|
-
return;
|
|
104
|
+
// Add all children from the content container
|
|
105
|
+
const children = [...content.children] as Container[];
|
|
106
|
+
if (children.length > 0) {
|
|
107
|
+
this.addItems(children);
|
|
192
108
|
}
|
|
109
|
+
}
|
|
193
110
|
|
|
194
|
-
|
|
111
|
+
/** Add a single item */
|
|
112
|
+
addItem(...items: Container[]): Container {
|
|
113
|
+
this.addItems(items);
|
|
114
|
+
return items[0];
|
|
195
115
|
}
|
|
196
116
|
|
|
197
117
|
/** Scroll to make a specific item/child visible */
|
|
198
118
|
scrollToItem(index: number): void {
|
|
199
|
-
|
|
200
|
-
const pos = index * this._config.snapSize;
|
|
201
|
-
if (this._config.direction === 'horizontal') {
|
|
202
|
-
this.scrollTo(pos, this._scrollY);
|
|
203
|
-
} else {
|
|
204
|
-
this.scrollTo(this._scrollX, pos);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
119
|
+
this.scrollTo(index);
|
|
207
120
|
}
|
|
208
121
|
|
|
209
122
|
/** Current scroll position */
|
|
210
123
|
get scrollPosition(): { x: number; y: number } {
|
|
211
|
-
return { x: this.
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Resize the scroll viewport */
|
|
215
|
-
resize(width: number, height: number): void {
|
|
216
|
-
this._config.width = width;
|
|
217
|
-
this._config.height = height;
|
|
218
|
-
|
|
219
|
-
// Redraw mask and background
|
|
220
|
-
this._mask.clear();
|
|
221
|
-
this._mask.roundRect(0, 0, width, height, this._config.borderRadius).fill(0xffffff);
|
|
222
|
-
|
|
223
|
-
this._bg.clear();
|
|
224
|
-
|
|
225
|
-
this.hitArea = { contains: (x: number, y: number) =>
|
|
226
|
-
x >= 0 && x <= width && y >= 0 && y <= height };
|
|
227
|
-
|
|
228
|
-
this.clampScroll();
|
|
229
|
-
this.applyScroll();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Destroy and clean up */
|
|
233
|
-
override destroy(options?: any): void {
|
|
234
|
-
this.stopAnimation();
|
|
235
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
236
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
237
|
-
}
|
|
238
|
-
this.off('pointerdown', this.onPointerDown);
|
|
239
|
-
this.off('pointermove', this.onPointerMove);
|
|
240
|
-
this.off('pointerup', this.onPointerUp);
|
|
241
|
-
this.off('pointerupoutside', this.onPointerUp);
|
|
242
|
-
this.off('wheel', this.onWheel);
|
|
243
|
-
super.destroy(options);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ─── Scroll mechanics ─────────────────────────────────
|
|
247
|
-
|
|
248
|
-
private get contentWidth(): number {
|
|
249
|
-
if (!this._content) return 0;
|
|
250
|
-
const bounds = this._content.getBounds();
|
|
251
|
-
return bounds.width;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private get contentHeight(): number {
|
|
255
|
-
if (!this._content) return 0;
|
|
256
|
-
const bounds = this._content.getBounds();
|
|
257
|
-
return bounds.height;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private get maxScrollX(): number {
|
|
261
|
-
return Math.max(0, this.contentWidth - this._config.width);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private get maxScrollY(): number {
|
|
265
|
-
return Math.max(0, this.contentHeight - this._config.height);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private canScrollX(): boolean {
|
|
269
|
-
return this._config.direction === 'horizontal' || this._config.direction === 'both';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private canScrollY(): boolean {
|
|
273
|
-
return this._config.direction === 'vertical' || this._config.direction === 'both';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private clampScroll(): void {
|
|
277
|
-
if (this.canScrollX()) {
|
|
278
|
-
this._scrollX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
279
|
-
} else {
|
|
280
|
-
this._scrollX = 0;
|
|
281
|
-
}
|
|
282
|
-
if (this.canScrollY()) {
|
|
283
|
-
this._scrollY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
284
|
-
} else {
|
|
285
|
-
this._scrollY = 0;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private applyScroll(): void {
|
|
290
|
-
if (!this._content) return;
|
|
291
|
-
this._content.x = -this._scrollX;
|
|
292
|
-
this._content.y = -this._scrollY;
|
|
293
|
-
this.updateScrollbars();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ─── Input handlers ────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
private onPointerDown = (e: any): void => {
|
|
299
|
-
this._isDragging = true;
|
|
300
|
-
this._isAnimating = false;
|
|
301
|
-
this.stopAnimation();
|
|
302
|
-
this.cursor = 'grabbing';
|
|
303
|
-
|
|
304
|
-
const local = e.getLocalPosition(this);
|
|
305
|
-
this._dragStart = { x: local.x, y: local.y };
|
|
306
|
-
this._scrollStart = { x: this._scrollX, y: this._scrollY };
|
|
307
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
308
|
-
this._lastDragTime = Date.now();
|
|
309
|
-
this._velocityX = 0;
|
|
310
|
-
this._velocityY = 0;
|
|
311
|
-
|
|
312
|
-
this.showScrollbars();
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
private onPointerMove = (e: any): void => {
|
|
316
|
-
if (!this._isDragging) return;
|
|
317
|
-
|
|
318
|
-
const local = e.getLocalPosition(this);
|
|
319
|
-
const dx = local.x - this._dragStart.x;
|
|
320
|
-
const dy = local.y - this._dragStart.y;
|
|
321
|
-
const now = Date.now();
|
|
322
|
-
const dt = Math.max(1, now - this._lastDragTime);
|
|
323
|
-
|
|
324
|
-
// Calculate velocity for inertia
|
|
325
|
-
this._velocityX = (local.x - this._lastDragPos.x) / dt * 16; // normalize to ~60fps
|
|
326
|
-
this._velocityY = (local.y - this._lastDragPos.y) / dt * 16;
|
|
327
|
-
|
|
328
|
-
this._lastDragPos = { x: local.x, y: local.y };
|
|
329
|
-
this._lastDragTime = now;
|
|
330
|
-
|
|
331
|
-
// Apply scroll with elasticity for overscroll
|
|
332
|
-
let newX = this._scrollStart.x - dx;
|
|
333
|
-
let newY = this._scrollStart.y - dy;
|
|
334
|
-
|
|
335
|
-
const elasticity = this._config.elasticity;
|
|
336
|
-
if (this.canScrollX()) {
|
|
337
|
-
if (newX < 0) newX *= elasticity;
|
|
338
|
-
else if (newX > this.maxScrollX) newX = this.maxScrollX + (newX - this.maxScrollX) * elasticity;
|
|
339
|
-
this._scrollX = newX;
|
|
340
|
-
}
|
|
341
|
-
if (this.canScrollY()) {
|
|
342
|
-
if (newY < 0) newY *= elasticity;
|
|
343
|
-
else if (newY > this.maxScrollY) newY = this.maxScrollY + (newY - this.maxScrollY) * elasticity;
|
|
344
|
-
this._scrollY = newY;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
this.applyScroll();
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
private onPointerUp = (): void => {
|
|
351
|
-
if (!this._isDragging) return;
|
|
352
|
-
this._isDragging = false;
|
|
353
|
-
this.cursor = 'grab';
|
|
354
|
-
|
|
355
|
-
// Start inertia
|
|
356
|
-
if (Math.abs(this._velocityX) > 0.5 || Math.abs(this._velocityY) > 0.5) {
|
|
357
|
-
this.startInertia();
|
|
358
|
-
} else {
|
|
359
|
-
this.snapAndBounce();
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
private onWheel = (e: any): void => {
|
|
364
|
-
e.preventDefault?.();
|
|
365
|
-
const delta = e.deltaY ?? 0;
|
|
366
|
-
const deltaX = e.deltaX ?? 0;
|
|
367
|
-
|
|
368
|
-
if (this.canScrollY()) {
|
|
369
|
-
this._scrollY += delta * 0.5;
|
|
370
|
-
}
|
|
371
|
-
if (this.canScrollX()) {
|
|
372
|
-
this._scrollX += deltaX * 0.5;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.clampScroll();
|
|
376
|
-
this.applyScroll();
|
|
377
|
-
this.showScrollbars();
|
|
378
|
-
this.scheduleScrollbarFade();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// ─── Inertia & snap ───────────────────────────────────
|
|
382
|
-
|
|
383
|
-
private startInertia(): void {
|
|
384
|
-
this._isAnimating = true;
|
|
385
|
-
|
|
386
|
-
const tick = () => {
|
|
387
|
-
if (!this._isAnimating) return;
|
|
388
|
-
|
|
389
|
-
this._velocityX *= this._config.inertia;
|
|
390
|
-
this._velocityY *= this._config.inertia;
|
|
391
|
-
|
|
392
|
-
if (this.canScrollX()) this._scrollX -= this._velocityX;
|
|
393
|
-
if (this.canScrollY()) this._scrollY -= this._velocityY;
|
|
394
|
-
|
|
395
|
-
// Bounce back if overscrolled
|
|
396
|
-
let bounced = false;
|
|
397
|
-
if (this.canScrollX()) {
|
|
398
|
-
if (this._scrollX < 0) { this._scrollX *= 0.8; bounced = true; }
|
|
399
|
-
else if (this._scrollX > this.maxScrollX) {
|
|
400
|
-
this._scrollX = this.maxScrollX + (this._scrollX - this.maxScrollX) * 0.8;
|
|
401
|
-
bounced = true;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (this.canScrollY()) {
|
|
405
|
-
if (this._scrollY < 0) { this._scrollY *= 0.8; bounced = true; }
|
|
406
|
-
else if (this._scrollY > this.maxScrollY) {
|
|
407
|
-
this._scrollY = this.maxScrollY + (this._scrollY - this.maxScrollY) * 0.8;
|
|
408
|
-
bounced = true;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
this.applyScroll();
|
|
413
|
-
|
|
414
|
-
const speed = Math.abs(this._velocityX) + Math.abs(this._velocityY);
|
|
415
|
-
if (speed < 0.1 && !bounced) {
|
|
416
|
-
this._isAnimating = false;
|
|
417
|
-
this.snapAndBounce();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private snapAndBounce(): void {
|
|
428
|
-
// Clamp first
|
|
429
|
-
let targetX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
|
|
430
|
-
let targetY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
|
|
431
|
-
|
|
432
|
-
// Snap
|
|
433
|
-
if (this._config.snapSize > 0) {
|
|
434
|
-
if (this.canScrollY()) {
|
|
435
|
-
targetY = Math.round(targetY / this._config.snapSize) * this._config.snapSize;
|
|
436
|
-
targetY = Math.max(0, Math.min(targetY, this.maxScrollY));
|
|
437
|
-
}
|
|
438
|
-
if (this.canScrollX()) {
|
|
439
|
-
targetX = Math.round(targetX / this._config.snapSize) * this._config.snapSize;
|
|
440
|
-
targetX = Math.max(0, Math.min(targetX, this.maxScrollX));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (Math.abs(targetX - this._scrollX) < 0.5 && Math.abs(targetY - this._scrollY) < 0.5) {
|
|
445
|
-
this._scrollX = targetX;
|
|
446
|
-
this._scrollY = targetY;
|
|
447
|
-
this.applyScroll();
|
|
448
|
-
this.scheduleScrollbarFade();
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
this.animateScrollTo(targetX, targetY);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private animateScrollTo(targetX: number, targetY: number): void {
|
|
456
|
-
this._isAnimating = true;
|
|
457
|
-
const startX = this._scrollX;
|
|
458
|
-
const startY = this._scrollY;
|
|
459
|
-
const startTime = Date.now();
|
|
460
|
-
const duration = 300;
|
|
461
|
-
|
|
462
|
-
const tick = () => {
|
|
463
|
-
if (!this._isAnimating) return;
|
|
464
|
-
|
|
465
|
-
const elapsed = Date.now() - startTime;
|
|
466
|
-
const t = Math.min(elapsed / duration, 1);
|
|
467
|
-
// easeOutCubic
|
|
468
|
-
const eased = 1 - Math.pow(1 - t, 3);
|
|
469
|
-
|
|
470
|
-
this._scrollX = startX + (targetX - startX) * eased;
|
|
471
|
-
this._scrollY = startY + (targetY - startY) * eased;
|
|
472
|
-
this.applyScroll();
|
|
473
|
-
|
|
474
|
-
if (t < 1) {
|
|
475
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
476
|
-
} else {
|
|
477
|
-
this._isAnimating = false;
|
|
478
|
-
this.scheduleScrollbarFade();
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
this._animationFrame = requestAnimationFrame(tick);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private stopAnimation(): void {
|
|
486
|
-
this._isAnimating = false;
|
|
487
|
-
if (this._animationFrame !== null) {
|
|
488
|
-
cancelAnimationFrame(this._animationFrame);
|
|
489
|
-
this._animationFrame = null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ─── Scrollbars ────────────────────────────────────────
|
|
494
|
-
|
|
495
|
-
private updateScrollbars(): void {
|
|
496
|
-
const { width, height, scrollbarWidth, scrollbarColor, scrollbarAlpha } = this._config;
|
|
497
|
-
|
|
498
|
-
if (this._scrollbarV && this.canScrollY() && this.contentHeight > height) {
|
|
499
|
-
const ratio = height / this.contentHeight;
|
|
500
|
-
const barH = Math.max(20, height * ratio);
|
|
501
|
-
const barY = (this._scrollY / this.maxScrollY) * (height - barH);
|
|
502
|
-
|
|
503
|
-
this._scrollbarV.clear();
|
|
504
|
-
this._scrollbarV.roundRect(
|
|
505
|
-
width - scrollbarWidth - 2,
|
|
506
|
-
Math.max(0, barY),
|
|
507
|
-
scrollbarWidth,
|
|
508
|
-
barH,
|
|
509
|
-
scrollbarWidth / 2,
|
|
510
|
-
).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (this._scrollbarH && this.canScrollX() && this.contentWidth > width) {
|
|
514
|
-
const ratio = width / this.contentWidth;
|
|
515
|
-
const barW = Math.max(20, width * ratio);
|
|
516
|
-
const barX = (this._scrollX / this.maxScrollX) * (width - barW);
|
|
517
|
-
|
|
518
|
-
this._scrollbarH.clear();
|
|
519
|
-
this._scrollbarH.roundRect(
|
|
520
|
-
Math.max(0, barX),
|
|
521
|
-
height - scrollbarWidth - 2,
|
|
522
|
-
barW,
|
|
523
|
-
scrollbarWidth,
|
|
524
|
-
scrollbarWidth / 2,
|
|
525
|
-
).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
private showScrollbars(): void {
|
|
530
|
-
if (this._scrollbarV) this._scrollbarV.alpha = 1;
|
|
531
|
-
if (this._scrollbarH) this._scrollbarH.alpha = 1;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private scheduleScrollbarFade(): void {
|
|
535
|
-
if (this._scrollbarFadeTimeout !== null) {
|
|
536
|
-
clearTimeout(this._scrollbarFadeTimeout);
|
|
537
|
-
}
|
|
538
|
-
this._scrollbarFadeTimeout = window.setTimeout(() => {
|
|
539
|
-
this.fadeScrollbars();
|
|
540
|
-
}, 1000);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
private fadeScrollbars(): void {
|
|
544
|
-
const duration = 300;
|
|
545
|
-
const startTime = Date.now();
|
|
546
|
-
const startAlphaV = this._scrollbarV?.alpha ?? 0;
|
|
547
|
-
const startAlphaH = this._scrollbarH?.alpha ?? 0;
|
|
548
|
-
|
|
549
|
-
const tick = () => {
|
|
550
|
-
const t = Math.min((Date.now() - startTime) / duration, 1);
|
|
551
|
-
if (this._scrollbarV) this._scrollbarV.alpha = startAlphaV * (1 - t);
|
|
552
|
-
if (this._scrollbarH) this._scrollbarH.alpha = startAlphaH * (1 - t);
|
|
553
|
-
if (t < 1) requestAnimationFrame(tick);
|
|
554
|
-
};
|
|
555
|
-
requestAnimationFrame(tick);
|
|
124
|
+
return { x: this.scrollX, y: this.scrollY };
|
|
556
125
|
}
|
|
557
126
|
}
|
package/src/ui/Toast.ts
CHANGED
|
@@ -38,9 +38,8 @@ export class Toast extends Container {
|
|
|
38
38
|
super();
|
|
39
39
|
|
|
40
40
|
this._config = {
|
|
41
|
-
duration: 3000,
|
|
42
|
-
bottomOffset: 60,
|
|
43
|
-
...config,
|
|
41
|
+
duration: config.duration ?? 3000,
|
|
42
|
+
bottomOffset: config.bottomOffset ?? 60,
|
|
44
43
|
};
|
|
45
44
|
|
|
46
45
|
this._bg = new Graphics();
|
|
@@ -69,7 +68,6 @@ export class Toast extends Container {
|
|
|
69
68
|
viewWidth?: number,
|
|
70
69
|
viewHeight?: number,
|
|
71
70
|
): Promise<void> {
|
|
72
|
-
// Clear previous dismiss
|
|
73
71
|
if (this._dismissTimeout) {
|
|
74
72
|
clearTimeout(this._dismissTimeout);
|
|
75
73
|
}
|
|
@@ -81,10 +79,10 @@ export class Toast extends Container {
|
|
|
81
79
|
const height = 44;
|
|
82
80
|
const radius = 8;
|
|
83
81
|
|
|
82
|
+
// Draw the background
|
|
84
83
|
this._bg.clear();
|
|
85
|
-
this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
|
|
86
|
-
this._bg.
|
|
87
|
-
.fill({ color: 0x000000, alpha: 0.2 });
|
|
84
|
+
this._bg.roundRect(-width / 2, -height / 2, width, height, radius);
|
|
85
|
+
this._bg.fill(TOAST_COLORS[type]);
|
|
88
86
|
|
|
89
87
|
// Position
|
|
90
88
|
if (viewWidth && viewHeight) {
|
|
@@ -96,10 +94,8 @@ export class Toast extends Container {
|
|
|
96
94
|
this.alpha = 0;
|
|
97
95
|
this.y += 20;
|
|
98
96
|
|
|
99
|
-
// Animate in
|
|
100
97
|
await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
|
|
101
98
|
|
|
102
|
-
// Auto-dismiss
|
|
103
99
|
if (this._config.duration > 0) {
|
|
104
100
|
this._dismissTimeout = setTimeout(() => {
|
|
105
101
|
this.dismiss();
|
package/src/ui/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// ─── @pixi/layout setup (must be imported before creating containers) ────
|
|
2
|
+
import '@pixi/layout';
|
|
3
|
+
|
|
4
|
+
// ─── Engine UI Components ─────────────────────────────────
|
|
1
5
|
export { Button } from './Button';
|
|
2
6
|
export type { ButtonConfig, ButtonState } from './Button';
|
|
3
7
|
export { ProgressBar } from './ProgressBar';
|
|
@@ -18,3 +22,12 @@ export { Layout } from './Layout';
|
|
|
18
22
|
export type { LayoutConfig, LayoutDirection, LayoutAlignment, LayoutAnchor } from './Layout';
|
|
19
23
|
export { ScrollContainer } from './ScrollContainer';
|
|
20
24
|
export type { ScrollContainerConfig, ScrollDirection } from './ScrollContainer';
|
|
25
|
+
|
|
26
|
+
// ─── Direct access to @pixi/ui and @pixi/layout ──────────
|
|
27
|
+
// These packages are optional peer dependencies.
|
|
28
|
+
// For any classes or types not wrapped by the engine (e.g. Slider, CheckBox,
|
|
29
|
+
// Input, Select, RadioGroup, List, etc.), import directly:
|
|
30
|
+
//
|
|
31
|
+
// import { Slider, CheckBox } from '@pixi/ui';
|
|
32
|
+
// import { LayoutContainer } from '@pixi/layout/components';
|
|
33
|
+
//
|