@energy8platform/game-engine 0.2.0 → 0.3.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 +318 -49
- package/dist/animation.cjs.js +191 -1
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.d.ts +117 -1
- package/dist/animation.esm.js +192 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/audio.cjs.js +66 -16
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +4 -0
- package/dist/audio.esm.js +66 -16
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +310 -84
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +60 -1
- package/dist/core.esm.js +311 -85
- package/dist/core.esm.js.map +1 -1
- package/dist/debug.cjs.js +36 -68
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +4 -6
- package/dist/debug.esm.js +36 -68
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +1250 -251
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +386 -41
- package/dist/index.esm.js +1250 -254
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +757 -1
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +208 -2
- package/dist/ui.esm.js +756 -2
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +65 -68
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +17 -23
- package/dist/vite.esm.js +66 -68
- package/dist/vite.esm.js.map +1 -1
- package/package.json +4 -5
- package/src/animation/SpriteAnimation.ts +210 -0
- package/src/animation/Tween.ts +27 -1
- package/src/animation/index.ts +2 -0
- package/src/audio/AudioManager.ts +64 -15
- package/src/core/EventEmitter.ts +7 -1
- package/src/core/GameApplication.ts +25 -7
- package/src/core/SceneManager.ts +3 -1
- package/src/debug/DevBridge.ts +49 -80
- package/src/index.ts +6 -0
- package/src/input/InputManager.ts +26 -0
- package/src/loading/CSSPreloader.ts +7 -33
- package/src/loading/LoadingScene.ts +17 -41
- package/src/loading/index.ts +1 -0
- package/src/loading/logo.ts +95 -0
- package/src/types.ts +4 -0
- package/src/ui/BalanceDisplay.ts +14 -0
- package/src/ui/Button.ts +1 -1
- package/src/ui/Layout.ts +364 -0
- package/src/ui/ScrollContainer.ts +557 -0
- package/src/ui/index.ts +4 -0
- package/src/viewport/ViewportManager.ts +2 -0
- package/src/vite/index.ts +83 -83
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { Container, Graphics } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
// ─── Types ───────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
|
|
6
|
+
|
|
7
|
+
export interface ScrollContainerConfig {
|
|
8
|
+
/** Visible viewport width */
|
|
9
|
+
width: number;
|
|
10
|
+
|
|
11
|
+
/** Visible viewport height */
|
|
12
|
+
height: number;
|
|
13
|
+
|
|
14
|
+
/** Scroll direction (default: 'vertical') */
|
|
15
|
+
direction?: ScrollDirection;
|
|
16
|
+
|
|
17
|
+
/** Show scrollbar(s) (default: true) */
|
|
18
|
+
showScrollbar?: boolean;
|
|
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;
|
|
28
|
+
|
|
29
|
+
/** Elasticity factor for overscroll bounce (0 = none, 1 = infinite, default: 0.3) */
|
|
30
|
+
elasticity?: number;
|
|
31
|
+
|
|
32
|
+
/** Inertia deceleration factor (0 = instant stop, 1 = infinite drift, default: 0.92) */
|
|
33
|
+
inertia?: number;
|
|
34
|
+
|
|
35
|
+
/** Snap to items of fixed height/width (0 = no snap) */
|
|
36
|
+
snapSize?: number;
|
|
37
|
+
|
|
38
|
+
/** Background color (undefined = transparent) */
|
|
39
|
+
backgroundColor?: number;
|
|
40
|
+
|
|
41
|
+
/** Background alpha (default: 1) */
|
|
42
|
+
backgroundAlpha?: number;
|
|
43
|
+
|
|
44
|
+
/** Border radius for the mask (default: 0) */
|
|
45
|
+
borderRadius?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Scrollable container with touch/drag, mouse wheel, inertia, and optional scrollbar.
|
|
50
|
+
*
|
|
51
|
+
* Perfect for paytables, settings panels, bet history, and any scrollable content
|
|
52
|
+
* that doesn't fit on screen.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const scroll = new ScrollContainer({
|
|
57
|
+
* width: 600,
|
|
58
|
+
* height: 400,
|
|
59
|
+
* direction: 'vertical',
|
|
60
|
+
* showScrollbar: true,
|
|
61
|
+
* elasticity: 0.3,
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Add content taller than 400px
|
|
65
|
+
* const list = new Container();
|
|
66
|
+
* for (let i = 0; i < 50; i++) {
|
|
67
|
+
* const row = createRow(i);
|
|
68
|
+
* row.y = i * 40;
|
|
69
|
+
* list.addChild(row);
|
|
70
|
+
* }
|
|
71
|
+
* scroll.setContent(list);
|
|
72
|
+
*
|
|
73
|
+
* scene.container.addChild(scroll);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export class ScrollContainer extends Container {
|
|
77
|
+
private _config: Required<
|
|
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;
|
|
103
|
+
|
|
104
|
+
constructor(config: ScrollContainerConfig) {
|
|
105
|
+
super();
|
|
106
|
+
|
|
107
|
+
this._config = {
|
|
108
|
+
width: config.width,
|
|
109
|
+
height: config.height,
|
|
110
|
+
direction: config.direction ?? 'vertical',
|
|
111
|
+
showScrollbar: config.showScrollbar ?? true,
|
|
112
|
+
scrollbarWidth: config.scrollbarWidth ?? 6,
|
|
113
|
+
scrollbarColor: config.scrollbarColor ?? 0xffffff,
|
|
114
|
+
scrollbarAlpha: config.scrollbarAlpha ?? 0.4,
|
|
115
|
+
elasticity: config.elasticity ?? 0.3,
|
|
116
|
+
inertia: config.inertia ?? 0.92,
|
|
117
|
+
snapSize: config.snapSize ?? 0,
|
|
118
|
+
borderRadius: config.borderRadius ?? 0,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Background
|
|
122
|
+
this._bg = new Graphics();
|
|
123
|
+
if (config.backgroundColor !== undefined) {
|
|
124
|
+
this._bg.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
|
|
125
|
+
.fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
|
|
126
|
+
}
|
|
127
|
+
this.addChild(this._bg);
|
|
128
|
+
|
|
129
|
+
// Viewport (masked area)
|
|
130
|
+
this._viewport = new Container();
|
|
131
|
+
this.addChild(this._viewport);
|
|
132
|
+
|
|
133
|
+
// Mask
|
|
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);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Set scrollable content. Replaces any existing content. */
|
|
168
|
+
setContent(content: Container): void {
|
|
169
|
+
if (this._content) {
|
|
170
|
+
this._viewport.removeChild(this._content);
|
|
171
|
+
}
|
|
172
|
+
this._content = content;
|
|
173
|
+
this._viewport.addChild(content);
|
|
174
|
+
this._scrollX = 0;
|
|
175
|
+
this._scrollY = 0;
|
|
176
|
+
this.applyScroll();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Get the content container */
|
|
180
|
+
get content(): Container | null {
|
|
181
|
+
return this._content;
|
|
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;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.animateScrollTo(x, y);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Scroll to make a specific item/child visible */
|
|
198
|
+
scrollToItem(index: number): void {
|
|
199
|
+
if (this._config.snapSize > 0) {
|
|
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
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Current scroll position */
|
|
210
|
+
get scrollPosition(): { x: number; y: number } {
|
|
211
|
+
return { x: this._scrollX, y: this._scrollY };
|
|
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);
|
|
556
|
+
}
|
|
557
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -14,3 +14,7 @@ export { Modal } from './Modal';
|
|
|
14
14
|
export type { ModalConfig } from './Modal';
|
|
15
15
|
export { Toast } from './Toast';
|
|
16
16
|
export type { ToastConfig, ToastType } from './Toast';
|
|
17
|
+
export { Layout } from './Layout';
|
|
18
|
+
export type { LayoutConfig, LayoutDirection, LayoutAlignment, LayoutAnchor } from './Layout';
|
|
19
|
+
export { ScrollContainer } from './ScrollContainer';
|
|
20
|
+
export type { ScrollContainerConfig, ScrollDirection } from './ScrollContainer';
|
|
@@ -205,6 +205,8 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
205
205
|
this._destroyed = true;
|
|
206
206
|
this._resizeObserver?.disconnect();
|
|
207
207
|
this._resizeObserver = null;
|
|
208
|
+
// Remove fallback window resize listener if it was used
|
|
209
|
+
window.removeEventListener('resize', this.onWindowResize);
|
|
208
210
|
if (this._resizeTimeout !== null) {
|
|
209
211
|
clearTimeout(this._resizeTimeout);
|
|
210
212
|
}
|