@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.
Files changed (59) hide show
  1. package/README.md +318 -49
  2. package/dist/animation.cjs.js +191 -1
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.d.ts +117 -1
  5. package/dist/animation.esm.js +192 -3
  6. package/dist/animation.esm.js.map +1 -1
  7. package/dist/audio.cjs.js +66 -16
  8. package/dist/audio.cjs.js.map +1 -1
  9. package/dist/audio.d.ts +4 -0
  10. package/dist/audio.esm.js +66 -16
  11. package/dist/audio.esm.js.map +1 -1
  12. package/dist/core.cjs.js +310 -84
  13. package/dist/core.cjs.js.map +1 -1
  14. package/dist/core.d.ts +60 -1
  15. package/dist/core.esm.js +311 -85
  16. package/dist/core.esm.js.map +1 -1
  17. package/dist/debug.cjs.js +36 -68
  18. package/dist/debug.cjs.js.map +1 -1
  19. package/dist/debug.d.ts +4 -6
  20. package/dist/debug.esm.js +36 -68
  21. package/dist/debug.esm.js.map +1 -1
  22. package/dist/index.cjs.js +1250 -251
  23. package/dist/index.cjs.js.map +1 -1
  24. package/dist/index.d.ts +386 -41
  25. package/dist/index.esm.js +1250 -254
  26. package/dist/index.esm.js.map +1 -1
  27. package/dist/ui.cjs.js +757 -1
  28. package/dist/ui.cjs.js.map +1 -1
  29. package/dist/ui.d.ts +208 -2
  30. package/dist/ui.esm.js +756 -2
  31. package/dist/ui.esm.js.map +1 -1
  32. package/dist/vite.cjs.js +65 -68
  33. package/dist/vite.cjs.js.map +1 -1
  34. package/dist/vite.d.ts +17 -23
  35. package/dist/vite.esm.js +66 -68
  36. package/dist/vite.esm.js.map +1 -1
  37. package/package.json +4 -5
  38. package/src/animation/SpriteAnimation.ts +210 -0
  39. package/src/animation/Tween.ts +27 -1
  40. package/src/animation/index.ts +2 -0
  41. package/src/audio/AudioManager.ts +64 -15
  42. package/src/core/EventEmitter.ts +7 -1
  43. package/src/core/GameApplication.ts +25 -7
  44. package/src/core/SceneManager.ts +3 -1
  45. package/src/debug/DevBridge.ts +49 -80
  46. package/src/index.ts +6 -0
  47. package/src/input/InputManager.ts +26 -0
  48. package/src/loading/CSSPreloader.ts +7 -33
  49. package/src/loading/LoadingScene.ts +17 -41
  50. package/src/loading/index.ts +1 -0
  51. package/src/loading/logo.ts +95 -0
  52. package/src/types.ts +4 -0
  53. package/src/ui/BalanceDisplay.ts +14 -0
  54. package/src/ui/Button.ts +1 -1
  55. package/src/ui/Layout.ts +364 -0
  56. package/src/ui/ScrollContainer.ts +557 -0
  57. package/src/ui/index.ts +4 -0
  58. package/src/viewport/ViewportManager.ts +2 -0
  59. package/src/vite/index.ts +83 -83
package/dist/ui.esm.js CHANGED
@@ -154,9 +154,20 @@ class Tween {
154
154
  }
155
155
  /**
156
156
  * Wait for a given duration (useful in timelines).
157
+ * Uses PixiJS Ticker for consistent timing with other tweens.
157
158
  */
158
159
  static delay(ms) {
159
- return new Promise((resolve) => setTimeout(resolve, ms));
160
+ return new Promise((resolve) => {
161
+ let elapsed = 0;
162
+ const onTick = (ticker) => {
163
+ elapsed += ticker.deltaMS;
164
+ if (elapsed >= ms) {
165
+ Ticker.shared.remove(onTick);
166
+ resolve();
167
+ }
168
+ };
169
+ Ticker.shared.add(onTick);
170
+ });
160
171
  }
161
172
  /**
162
173
  * Kill all tweens on a target.
@@ -183,6 +194,20 @@ class Tween {
183
194
  static get activeTweens() {
184
195
  return Tween._tweens.length;
185
196
  }
197
+ /**
198
+ * Reset the tween system — kill all tweens and remove the ticker.
199
+ * Useful for cleanup between game instances, tests, or hot-reload.
200
+ */
201
+ static reset() {
202
+ for (const tw of Tween._tweens) {
203
+ tw.resolve();
204
+ }
205
+ Tween._tweens.length = 0;
206
+ if (Tween._tickerAdded) {
207
+ Ticker.shared.remove(Tween.tick);
208
+ Tween._tickerAdded = false;
209
+ }
210
+ }
186
211
  // ─── Internal ──────────────────────────────────────────
187
212
  static ensureTicker() {
188
213
  if (Tween._tickerAdded)
@@ -694,6 +719,7 @@ class BalanceDisplay extends Container {
694
719
  _currentValue = 0;
695
720
  _displayedValue = 0;
696
721
  _animating = false;
722
+ _animationCancelled = false;
697
723
  constructor(config = {}) {
698
724
  super();
699
725
  this._config = {
@@ -755,11 +781,22 @@ class BalanceDisplay extends Container {
755
781
  this.updateDisplay();
756
782
  }
757
783
  async animateValue(from, to) {
784
+ // Cancel any ongoing animation
785
+ if (this._animating) {
786
+ this._animationCancelled = true;
787
+ }
758
788
  this._animating = true;
789
+ this._animationCancelled = false;
759
790
  const duration = this._config.animationDuration;
760
791
  const startTime = Date.now();
761
792
  return new Promise((resolve) => {
762
793
  const tick = () => {
794
+ // If cancelled by a newer animation, stop immediately
795
+ if (this._animationCancelled) {
796
+ this._animating = false;
797
+ resolve();
798
+ return;
799
+ }
763
800
  const elapsed = Date.now() - startTime;
764
801
  const t = Math.min(elapsed / duration, 1);
765
802
  const eased = Easing.easeOutCubic(t);
@@ -1068,5 +1105,722 @@ class Toast extends Container {
1068
1105
  }
1069
1106
  }
1070
1107
 
1071
- export { BalanceDisplay, Button, Label, Modal, Panel, ProgressBar, Toast, WinDisplay };
1108
+ /**
1109
+ * Responsive layout container that automatically arranges its children.
1110
+ *
1111
+ * Supports horizontal, vertical, grid, and wrap layout modes with
1112
+ * alignment, padding, gap, and viewport-anchor positioning.
1113
+ * Breakpoints allow different layouts for different screen sizes.
1114
+ *
1115
+ * @example
1116
+ * ```ts
1117
+ * const toolbar = new Layout({
1118
+ * direction: 'horizontal',
1119
+ * gap: 20,
1120
+ * alignment: 'center',
1121
+ * anchor: 'bottom-center',
1122
+ * padding: 16,
1123
+ * breakpoints: {
1124
+ * 768: { direction: 'vertical', gap: 10 },
1125
+ * },
1126
+ * });
1127
+ *
1128
+ * toolbar.addItem(spinButton);
1129
+ * toolbar.addItem(betLabel);
1130
+ * scene.container.addChild(toolbar);
1131
+ *
1132
+ * // On resize, update layout position relative to viewport
1133
+ * toolbar.updateViewport(width, height);
1134
+ * ```
1135
+ */
1136
+ class Layout extends Container {
1137
+ _config;
1138
+ _padding;
1139
+ _anchor;
1140
+ _maxWidth;
1141
+ _breakpoints;
1142
+ _content;
1143
+ _items = [];
1144
+ _viewportWidth = 0;
1145
+ _viewportHeight = 0;
1146
+ constructor(config = {}) {
1147
+ super();
1148
+ this._config = {
1149
+ direction: config.direction ?? 'vertical',
1150
+ gap: config.gap ?? 0,
1151
+ alignment: config.alignment ?? 'start',
1152
+ autoLayout: config.autoLayout ?? true,
1153
+ columns: config.columns ?? 2,
1154
+ };
1155
+ this._padding = Layout.normalizePadding(config.padding ?? 0);
1156
+ this._anchor = config.anchor ?? 'top-left';
1157
+ this._maxWidth = config.maxWidth ?? Infinity;
1158
+ // Sort breakpoints by width ascending for correct resolution
1159
+ this._breakpoints = config.breakpoints
1160
+ ? Object.entries(config.breakpoints)
1161
+ .map(([w, cfg]) => [Number(w), cfg])
1162
+ .sort((a, b) => a[0] - b[0])
1163
+ : [];
1164
+ this._content = new Container();
1165
+ this.addChild(this._content);
1166
+ }
1167
+ /** Add an item to the layout */
1168
+ addItem(child) {
1169
+ this._items.push(child);
1170
+ this._content.addChild(child);
1171
+ if (this._config.autoLayout)
1172
+ this.layout();
1173
+ return this;
1174
+ }
1175
+ /** Remove an item from the layout */
1176
+ removeItem(child) {
1177
+ const idx = this._items.indexOf(child);
1178
+ if (idx !== -1) {
1179
+ this._items.splice(idx, 1);
1180
+ this._content.removeChild(child);
1181
+ if (this._config.autoLayout)
1182
+ this.layout();
1183
+ }
1184
+ return this;
1185
+ }
1186
+ /** Remove all items */
1187
+ clearItems() {
1188
+ for (const item of this._items) {
1189
+ this._content.removeChild(item);
1190
+ }
1191
+ this._items.length = 0;
1192
+ if (this._config.autoLayout)
1193
+ this.layout();
1194
+ return this;
1195
+ }
1196
+ /** Get all layout items */
1197
+ get items() {
1198
+ return this._items;
1199
+ }
1200
+ /**
1201
+ * Update the viewport size and recalculate layout.
1202
+ * Should be called from `Scene.onResize()`.
1203
+ */
1204
+ updateViewport(width, height) {
1205
+ this._viewportWidth = width;
1206
+ this._viewportHeight = height;
1207
+ this.layout();
1208
+ }
1209
+ /**
1210
+ * Recalculate layout positions of all children.
1211
+ */
1212
+ layout() {
1213
+ if (this._items.length === 0)
1214
+ return;
1215
+ // Resolve effective config (apply breakpoint overrides)
1216
+ const effective = this.resolveConfig();
1217
+ const gap = effective.gap ?? this._config.gap;
1218
+ const direction = effective.direction ?? this._config.direction;
1219
+ const alignment = effective.alignment ?? this._config.alignment;
1220
+ const columns = effective.columns ?? this._config.columns;
1221
+ const padding = effective.padding !== undefined
1222
+ ? Layout.normalizePadding(effective.padding)
1223
+ : this._padding;
1224
+ const maxWidth = effective.maxWidth ?? this._maxWidth;
1225
+ const [pt, pr, pb, pl] = padding;
1226
+ switch (direction) {
1227
+ case 'horizontal':
1228
+ this.layoutLinear('x', 'y', gap, alignment, pl, pt);
1229
+ break;
1230
+ case 'vertical':
1231
+ this.layoutLinear('y', 'x', gap, alignment, pt, pl);
1232
+ break;
1233
+ case 'grid':
1234
+ this.layoutGrid(columns, gap, alignment, pl, pt);
1235
+ break;
1236
+ case 'wrap':
1237
+ this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
1238
+ break;
1239
+ }
1240
+ // Apply anchor positioning relative to viewport
1241
+ this.applyAnchor(effective.anchor ?? this._anchor);
1242
+ }
1243
+ // ─── Private layout helpers ────────────────────────────
1244
+ layoutLinear(mainAxis, crossAxis, gap, alignment, mainOffset, crossOffset) {
1245
+ let pos = mainOffset;
1246
+ const sizes = this._items.map(item => this.getItemSize(item));
1247
+ const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
1248
+ for (let i = 0; i < this._items.length; i++) {
1249
+ const item = this._items[i];
1250
+ const size = sizes[i];
1251
+ item[mainAxis] = pos;
1252
+ // Cross-axis alignment
1253
+ const itemCross = crossAxis === 'x' ? size.width : size.height;
1254
+ switch (alignment) {
1255
+ case 'start':
1256
+ item[crossAxis] = crossOffset;
1257
+ break;
1258
+ case 'center':
1259
+ item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
1260
+ break;
1261
+ case 'end':
1262
+ item[crossAxis] = crossOffset + maxCross - itemCross;
1263
+ break;
1264
+ case 'stretch':
1265
+ item[crossAxis] = crossOffset;
1266
+ // Note: stretch doesn't resize children — that's up to the item
1267
+ break;
1268
+ }
1269
+ const mainSize = mainAxis === 'x' ? size.width : size.height;
1270
+ pos += mainSize + gap;
1271
+ }
1272
+ }
1273
+ layoutGrid(columns, gap, alignment, offsetX, offsetY) {
1274
+ const sizes = this._items.map(item => this.getItemSize(item));
1275
+ const maxItemWidth = Math.max(...sizes.map(s => s.width));
1276
+ const maxItemHeight = Math.max(...sizes.map(s => s.height));
1277
+ const cellW = maxItemWidth + gap;
1278
+ const cellH = maxItemHeight + gap;
1279
+ for (let i = 0; i < this._items.length; i++) {
1280
+ const item = this._items[i];
1281
+ const col = i % columns;
1282
+ const row = Math.floor(i / columns);
1283
+ const size = sizes[i];
1284
+ // X alignment within cell
1285
+ switch (alignment) {
1286
+ case 'center':
1287
+ item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
1288
+ break;
1289
+ case 'end':
1290
+ item.x = offsetX + col * cellW + maxItemWidth - size.width;
1291
+ break;
1292
+ default:
1293
+ item.x = offsetX + col * cellW;
1294
+ }
1295
+ item.y = offsetY + row * cellH;
1296
+ }
1297
+ }
1298
+ layoutWrap(maxWidth, gap, alignment, offsetX, offsetY) {
1299
+ let x = offsetX;
1300
+ let y = offsetY;
1301
+ let rowHeight = 0;
1302
+ const sizes = this._items.map(item => this.getItemSize(item));
1303
+ for (let i = 0; i < this._items.length; i++) {
1304
+ const item = this._items[i];
1305
+ const size = sizes[i];
1306
+ // Check if item fits in current row
1307
+ if (x + size.width > maxWidth + offsetX && x > offsetX) {
1308
+ // Wrap to next row
1309
+ x = offsetX;
1310
+ y += rowHeight + gap;
1311
+ rowHeight = 0;
1312
+ }
1313
+ item.x = x;
1314
+ item.y = y;
1315
+ x += size.width + gap;
1316
+ rowHeight = Math.max(rowHeight, size.height);
1317
+ }
1318
+ }
1319
+ applyAnchor(anchor) {
1320
+ if (this._viewportWidth === 0 || this._viewportHeight === 0)
1321
+ return;
1322
+ const bounds = this._content.getBounds();
1323
+ const contentW = bounds.width;
1324
+ const contentH = bounds.height;
1325
+ const vw = this._viewportWidth;
1326
+ const vh = this._viewportHeight;
1327
+ let anchorX = 0;
1328
+ let anchorY = 0;
1329
+ // Horizontal
1330
+ if (anchor.includes('left')) {
1331
+ anchorX = 0;
1332
+ }
1333
+ else if (anchor.includes('right')) {
1334
+ anchorX = vw - contentW;
1335
+ }
1336
+ else {
1337
+ // center
1338
+ anchorX = (vw - contentW) / 2;
1339
+ }
1340
+ // Vertical
1341
+ if (anchor.startsWith('top')) {
1342
+ anchorY = 0;
1343
+ }
1344
+ else if (anchor.startsWith('bottom')) {
1345
+ anchorY = vh - contentH;
1346
+ }
1347
+ else {
1348
+ // center
1349
+ anchorY = (vh - contentH) / 2;
1350
+ }
1351
+ // Compensate for content's local bounds offset
1352
+ this.x = anchorX - bounds.x;
1353
+ this.y = anchorY - bounds.y;
1354
+ }
1355
+ resolveConfig() {
1356
+ if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
1357
+ return {};
1358
+ }
1359
+ // Find the largest breakpoint that's ≤ current viewport width
1360
+ let resolved = {};
1361
+ for (const [maxWidth, overrides] of this._breakpoints) {
1362
+ if (this._viewportWidth <= maxWidth) {
1363
+ resolved = overrides;
1364
+ break;
1365
+ }
1366
+ }
1367
+ return resolved;
1368
+ }
1369
+ getItemSize(item) {
1370
+ const bounds = item.getBounds();
1371
+ return { width: bounds.width, height: bounds.height };
1372
+ }
1373
+ static normalizePadding(padding) {
1374
+ if (typeof padding === 'number') {
1375
+ return [padding, padding, padding, padding];
1376
+ }
1377
+ return padding;
1378
+ }
1379
+ }
1380
+
1381
+ /**
1382
+ * Scrollable container with touch/drag, mouse wheel, inertia, and optional scrollbar.
1383
+ *
1384
+ * Perfect for paytables, settings panels, bet history, and any scrollable content
1385
+ * that doesn't fit on screen.
1386
+ *
1387
+ * @example
1388
+ * ```ts
1389
+ * const scroll = new ScrollContainer({
1390
+ * width: 600,
1391
+ * height: 400,
1392
+ * direction: 'vertical',
1393
+ * showScrollbar: true,
1394
+ * elasticity: 0.3,
1395
+ * });
1396
+ *
1397
+ * // Add content taller than 400px
1398
+ * const list = new Container();
1399
+ * for (let i = 0; i < 50; i++) {
1400
+ * const row = createRow(i);
1401
+ * row.y = i * 40;
1402
+ * list.addChild(row);
1403
+ * }
1404
+ * scroll.setContent(list);
1405
+ *
1406
+ * scene.container.addChild(scroll);
1407
+ * ```
1408
+ */
1409
+ class ScrollContainer extends Container {
1410
+ _config;
1411
+ _viewport;
1412
+ _content = null;
1413
+ _mask;
1414
+ _bg;
1415
+ _scrollbarV = null;
1416
+ _scrollbarH = null;
1417
+ _scrollbarFadeTimeout = null;
1418
+ // Scroll state
1419
+ _scrollX = 0;
1420
+ _scrollY = 0;
1421
+ _velocityX = 0;
1422
+ _velocityY = 0;
1423
+ _isDragging = false;
1424
+ _dragStart = { x: 0, y: 0 };
1425
+ _scrollStart = { x: 0, y: 0 };
1426
+ _lastDragPos = { x: 0, y: 0 };
1427
+ _lastDragTime = 0;
1428
+ _isAnimating = false;
1429
+ _animationFrame = null;
1430
+ constructor(config) {
1431
+ super();
1432
+ this._config = {
1433
+ width: config.width,
1434
+ height: config.height,
1435
+ direction: config.direction ?? 'vertical',
1436
+ showScrollbar: config.showScrollbar ?? true,
1437
+ scrollbarWidth: config.scrollbarWidth ?? 6,
1438
+ scrollbarColor: config.scrollbarColor ?? 0xffffff,
1439
+ scrollbarAlpha: config.scrollbarAlpha ?? 0.4,
1440
+ elasticity: config.elasticity ?? 0.3,
1441
+ inertia: config.inertia ?? 0.92,
1442
+ snapSize: config.snapSize ?? 0,
1443
+ borderRadius: config.borderRadius ?? 0,
1444
+ };
1445
+ // Background
1446
+ this._bg = new Graphics();
1447
+ if (config.backgroundColor !== undefined) {
1448
+ this._bg.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
1449
+ .fill({ color: config.backgroundColor, alpha: config.backgroundAlpha ?? 1 });
1450
+ }
1451
+ this.addChild(this._bg);
1452
+ // Viewport (masked area)
1453
+ this._viewport = new Container();
1454
+ this.addChild(this._viewport);
1455
+ // Mask
1456
+ this._mask = new Graphics();
1457
+ this._mask.roundRect(0, 0, config.width, config.height, this._config.borderRadius)
1458
+ .fill(0xffffff);
1459
+ this.addChild(this._mask);
1460
+ this._viewport.mask = this._mask;
1461
+ // Scrollbars
1462
+ if (this._config.showScrollbar) {
1463
+ if (this._config.direction !== 'horizontal') {
1464
+ this._scrollbarV = new Graphics();
1465
+ this._scrollbarV.alpha = 0;
1466
+ this.addChild(this._scrollbarV);
1467
+ }
1468
+ if (this._config.direction !== 'vertical') {
1469
+ this._scrollbarH = new Graphics();
1470
+ this._scrollbarH.alpha = 0;
1471
+ this.addChild(this._scrollbarH);
1472
+ }
1473
+ }
1474
+ // Interaction
1475
+ this.eventMode = 'static';
1476
+ this.cursor = 'grab';
1477
+ this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
1478
+ this.on('pointerdown', this.onPointerDown);
1479
+ this.on('pointermove', this.onPointerMove);
1480
+ this.on('pointerup', this.onPointerUp);
1481
+ this.on('pointerupoutside', this.onPointerUp);
1482
+ this.on('wheel', this.onWheel);
1483
+ }
1484
+ /** Set scrollable content. Replaces any existing content. */
1485
+ setContent(content) {
1486
+ if (this._content) {
1487
+ this._viewport.removeChild(this._content);
1488
+ }
1489
+ this._content = content;
1490
+ this._viewport.addChild(content);
1491
+ this._scrollX = 0;
1492
+ this._scrollY = 0;
1493
+ this.applyScroll();
1494
+ }
1495
+ /** Get the content container */
1496
+ get content() {
1497
+ return this._content;
1498
+ }
1499
+ /** Scroll to a specific position (in content coordinates) */
1500
+ scrollTo(x, y, animate = true) {
1501
+ if (!animate) {
1502
+ this._scrollX = x;
1503
+ this._scrollY = y;
1504
+ this.clampScroll();
1505
+ this.applyScroll();
1506
+ return;
1507
+ }
1508
+ this.animateScrollTo(x, y);
1509
+ }
1510
+ /** Scroll to make a specific item/child visible */
1511
+ scrollToItem(index) {
1512
+ if (this._config.snapSize > 0) {
1513
+ const pos = index * this._config.snapSize;
1514
+ if (this._config.direction === 'horizontal') {
1515
+ this.scrollTo(pos, this._scrollY);
1516
+ }
1517
+ else {
1518
+ this.scrollTo(this._scrollX, pos);
1519
+ }
1520
+ }
1521
+ }
1522
+ /** Current scroll position */
1523
+ get scrollPosition() {
1524
+ return { x: this._scrollX, y: this._scrollY };
1525
+ }
1526
+ /** Resize the scroll viewport */
1527
+ resize(width, height) {
1528
+ this._config.width = width;
1529
+ this._config.height = height;
1530
+ // Redraw mask and background
1531
+ this._mask.clear();
1532
+ this._mask.roundRect(0, 0, width, height, this._config.borderRadius).fill(0xffffff);
1533
+ this._bg.clear();
1534
+ this.hitArea = { contains: (x, y) => x >= 0 && x <= width && y >= 0 && y <= height };
1535
+ this.clampScroll();
1536
+ this.applyScroll();
1537
+ }
1538
+ /** Destroy and clean up */
1539
+ destroy(options) {
1540
+ this.stopAnimation();
1541
+ if (this._scrollbarFadeTimeout !== null) {
1542
+ clearTimeout(this._scrollbarFadeTimeout);
1543
+ }
1544
+ this.off('pointerdown', this.onPointerDown);
1545
+ this.off('pointermove', this.onPointerMove);
1546
+ this.off('pointerup', this.onPointerUp);
1547
+ this.off('pointerupoutside', this.onPointerUp);
1548
+ this.off('wheel', this.onWheel);
1549
+ super.destroy(options);
1550
+ }
1551
+ // ─── Scroll mechanics ─────────────────────────────────
1552
+ get contentWidth() {
1553
+ if (!this._content)
1554
+ return 0;
1555
+ const bounds = this._content.getBounds();
1556
+ return bounds.width;
1557
+ }
1558
+ get contentHeight() {
1559
+ if (!this._content)
1560
+ return 0;
1561
+ const bounds = this._content.getBounds();
1562
+ return bounds.height;
1563
+ }
1564
+ get maxScrollX() {
1565
+ return Math.max(0, this.contentWidth - this._config.width);
1566
+ }
1567
+ get maxScrollY() {
1568
+ return Math.max(0, this.contentHeight - this._config.height);
1569
+ }
1570
+ canScrollX() {
1571
+ return this._config.direction === 'horizontal' || this._config.direction === 'both';
1572
+ }
1573
+ canScrollY() {
1574
+ return this._config.direction === 'vertical' || this._config.direction === 'both';
1575
+ }
1576
+ clampScroll() {
1577
+ if (this.canScrollX()) {
1578
+ this._scrollX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
1579
+ }
1580
+ else {
1581
+ this._scrollX = 0;
1582
+ }
1583
+ if (this.canScrollY()) {
1584
+ this._scrollY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
1585
+ }
1586
+ else {
1587
+ this._scrollY = 0;
1588
+ }
1589
+ }
1590
+ applyScroll() {
1591
+ if (!this._content)
1592
+ return;
1593
+ this._content.x = -this._scrollX;
1594
+ this._content.y = -this._scrollY;
1595
+ this.updateScrollbars();
1596
+ }
1597
+ // ─── Input handlers ────────────────────────────────────
1598
+ onPointerDown = (e) => {
1599
+ this._isDragging = true;
1600
+ this._isAnimating = false;
1601
+ this.stopAnimation();
1602
+ this.cursor = 'grabbing';
1603
+ const local = e.getLocalPosition(this);
1604
+ this._dragStart = { x: local.x, y: local.y };
1605
+ this._scrollStart = { x: this._scrollX, y: this._scrollY };
1606
+ this._lastDragPos = { x: local.x, y: local.y };
1607
+ this._lastDragTime = Date.now();
1608
+ this._velocityX = 0;
1609
+ this._velocityY = 0;
1610
+ this.showScrollbars();
1611
+ };
1612
+ onPointerMove = (e) => {
1613
+ if (!this._isDragging)
1614
+ return;
1615
+ const local = e.getLocalPosition(this);
1616
+ const dx = local.x - this._dragStart.x;
1617
+ const dy = local.y - this._dragStart.y;
1618
+ const now = Date.now();
1619
+ const dt = Math.max(1, now - this._lastDragTime);
1620
+ // Calculate velocity for inertia
1621
+ this._velocityX = (local.x - this._lastDragPos.x) / dt * 16; // normalize to ~60fps
1622
+ this._velocityY = (local.y - this._lastDragPos.y) / dt * 16;
1623
+ this._lastDragPos = { x: local.x, y: local.y };
1624
+ this._lastDragTime = now;
1625
+ // Apply scroll with elasticity for overscroll
1626
+ let newX = this._scrollStart.x - dx;
1627
+ let newY = this._scrollStart.y - dy;
1628
+ const elasticity = this._config.elasticity;
1629
+ if (this.canScrollX()) {
1630
+ if (newX < 0)
1631
+ newX *= elasticity;
1632
+ else if (newX > this.maxScrollX)
1633
+ newX = this.maxScrollX + (newX - this.maxScrollX) * elasticity;
1634
+ this._scrollX = newX;
1635
+ }
1636
+ if (this.canScrollY()) {
1637
+ if (newY < 0)
1638
+ newY *= elasticity;
1639
+ else if (newY > this.maxScrollY)
1640
+ newY = this.maxScrollY + (newY - this.maxScrollY) * elasticity;
1641
+ this._scrollY = newY;
1642
+ }
1643
+ this.applyScroll();
1644
+ };
1645
+ onPointerUp = () => {
1646
+ if (!this._isDragging)
1647
+ return;
1648
+ this._isDragging = false;
1649
+ this.cursor = 'grab';
1650
+ // Start inertia
1651
+ if (Math.abs(this._velocityX) > 0.5 || Math.abs(this._velocityY) > 0.5) {
1652
+ this.startInertia();
1653
+ }
1654
+ else {
1655
+ this.snapAndBounce();
1656
+ }
1657
+ };
1658
+ onWheel = (e) => {
1659
+ e.preventDefault?.();
1660
+ const delta = e.deltaY ?? 0;
1661
+ const deltaX = e.deltaX ?? 0;
1662
+ if (this.canScrollY()) {
1663
+ this._scrollY += delta * 0.5;
1664
+ }
1665
+ if (this.canScrollX()) {
1666
+ this._scrollX += deltaX * 0.5;
1667
+ }
1668
+ this.clampScroll();
1669
+ this.applyScroll();
1670
+ this.showScrollbars();
1671
+ this.scheduleScrollbarFade();
1672
+ };
1673
+ // ─── Inertia & snap ───────────────────────────────────
1674
+ startInertia() {
1675
+ this._isAnimating = true;
1676
+ const tick = () => {
1677
+ if (!this._isAnimating)
1678
+ return;
1679
+ this._velocityX *= this._config.inertia;
1680
+ this._velocityY *= this._config.inertia;
1681
+ if (this.canScrollX())
1682
+ this._scrollX -= this._velocityX;
1683
+ if (this.canScrollY())
1684
+ this._scrollY -= this._velocityY;
1685
+ // Bounce back if overscrolled
1686
+ let bounced = false;
1687
+ if (this.canScrollX()) {
1688
+ if (this._scrollX < 0) {
1689
+ this._scrollX *= 0.8;
1690
+ bounced = true;
1691
+ }
1692
+ else if (this._scrollX > this.maxScrollX) {
1693
+ this._scrollX = this.maxScrollX + (this._scrollX - this.maxScrollX) * 0.8;
1694
+ bounced = true;
1695
+ }
1696
+ }
1697
+ if (this.canScrollY()) {
1698
+ if (this._scrollY < 0) {
1699
+ this._scrollY *= 0.8;
1700
+ bounced = true;
1701
+ }
1702
+ else if (this._scrollY > this.maxScrollY) {
1703
+ this._scrollY = this.maxScrollY + (this._scrollY - this.maxScrollY) * 0.8;
1704
+ bounced = true;
1705
+ }
1706
+ }
1707
+ this.applyScroll();
1708
+ const speed = Math.abs(this._velocityX) + Math.abs(this._velocityY);
1709
+ if (speed < 0.1 && !bounced) {
1710
+ this._isAnimating = false;
1711
+ this.snapAndBounce();
1712
+ return;
1713
+ }
1714
+ this._animationFrame = requestAnimationFrame(tick);
1715
+ };
1716
+ this._animationFrame = requestAnimationFrame(tick);
1717
+ }
1718
+ snapAndBounce() {
1719
+ // Clamp first
1720
+ let targetX = Math.max(0, Math.min(this._scrollX, this.maxScrollX));
1721
+ let targetY = Math.max(0, Math.min(this._scrollY, this.maxScrollY));
1722
+ // Snap
1723
+ if (this._config.snapSize > 0) {
1724
+ if (this.canScrollY()) {
1725
+ targetY = Math.round(targetY / this._config.snapSize) * this._config.snapSize;
1726
+ targetY = Math.max(0, Math.min(targetY, this.maxScrollY));
1727
+ }
1728
+ if (this.canScrollX()) {
1729
+ targetX = Math.round(targetX / this._config.snapSize) * this._config.snapSize;
1730
+ targetX = Math.max(0, Math.min(targetX, this.maxScrollX));
1731
+ }
1732
+ }
1733
+ if (Math.abs(targetX - this._scrollX) < 0.5 && Math.abs(targetY - this._scrollY) < 0.5) {
1734
+ this._scrollX = targetX;
1735
+ this._scrollY = targetY;
1736
+ this.applyScroll();
1737
+ this.scheduleScrollbarFade();
1738
+ return;
1739
+ }
1740
+ this.animateScrollTo(targetX, targetY);
1741
+ }
1742
+ animateScrollTo(targetX, targetY) {
1743
+ this._isAnimating = true;
1744
+ const startX = this._scrollX;
1745
+ const startY = this._scrollY;
1746
+ const startTime = Date.now();
1747
+ const duration = 300;
1748
+ const tick = () => {
1749
+ if (!this._isAnimating)
1750
+ return;
1751
+ const elapsed = Date.now() - startTime;
1752
+ const t = Math.min(elapsed / duration, 1);
1753
+ // easeOutCubic
1754
+ const eased = 1 - Math.pow(1 - t, 3);
1755
+ this._scrollX = startX + (targetX - startX) * eased;
1756
+ this._scrollY = startY + (targetY - startY) * eased;
1757
+ this.applyScroll();
1758
+ if (t < 1) {
1759
+ this._animationFrame = requestAnimationFrame(tick);
1760
+ }
1761
+ else {
1762
+ this._isAnimating = false;
1763
+ this.scheduleScrollbarFade();
1764
+ }
1765
+ };
1766
+ this._animationFrame = requestAnimationFrame(tick);
1767
+ }
1768
+ stopAnimation() {
1769
+ this._isAnimating = false;
1770
+ if (this._animationFrame !== null) {
1771
+ cancelAnimationFrame(this._animationFrame);
1772
+ this._animationFrame = null;
1773
+ }
1774
+ }
1775
+ // ─── Scrollbars ────────────────────────────────────────
1776
+ updateScrollbars() {
1777
+ const { width, height, scrollbarWidth, scrollbarColor, scrollbarAlpha } = this._config;
1778
+ if (this._scrollbarV && this.canScrollY() && this.contentHeight > height) {
1779
+ const ratio = height / this.contentHeight;
1780
+ const barH = Math.max(20, height * ratio);
1781
+ const barY = (this._scrollY / this.maxScrollY) * (height - barH);
1782
+ this._scrollbarV.clear();
1783
+ this._scrollbarV.roundRect(width - scrollbarWidth - 2, Math.max(0, barY), scrollbarWidth, barH, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
1784
+ }
1785
+ if (this._scrollbarH && this.canScrollX() && this.contentWidth > width) {
1786
+ const ratio = width / this.contentWidth;
1787
+ const barW = Math.max(20, width * ratio);
1788
+ const barX = (this._scrollX / this.maxScrollX) * (width - barW);
1789
+ this._scrollbarH.clear();
1790
+ this._scrollbarH.roundRect(Math.max(0, barX), height - scrollbarWidth - 2, barW, scrollbarWidth, scrollbarWidth / 2).fill({ color: scrollbarColor, alpha: scrollbarAlpha });
1791
+ }
1792
+ }
1793
+ showScrollbars() {
1794
+ if (this._scrollbarV)
1795
+ this._scrollbarV.alpha = 1;
1796
+ if (this._scrollbarH)
1797
+ this._scrollbarH.alpha = 1;
1798
+ }
1799
+ scheduleScrollbarFade() {
1800
+ if (this._scrollbarFadeTimeout !== null) {
1801
+ clearTimeout(this._scrollbarFadeTimeout);
1802
+ }
1803
+ this._scrollbarFadeTimeout = window.setTimeout(() => {
1804
+ this.fadeScrollbars();
1805
+ }, 1000);
1806
+ }
1807
+ fadeScrollbars() {
1808
+ const duration = 300;
1809
+ const startTime = Date.now();
1810
+ const startAlphaV = this._scrollbarV?.alpha ?? 0;
1811
+ const startAlphaH = this._scrollbarH?.alpha ?? 0;
1812
+ const tick = () => {
1813
+ const t = Math.min((Date.now() - startTime) / duration, 1);
1814
+ if (this._scrollbarV)
1815
+ this._scrollbarV.alpha = startAlphaV * (1 - t);
1816
+ if (this._scrollbarH)
1817
+ this._scrollbarH.alpha = startAlphaH * (1 - t);
1818
+ if (t < 1)
1819
+ requestAnimationFrame(tick);
1820
+ };
1821
+ requestAnimationFrame(tick);
1822
+ }
1823
+ }
1824
+
1825
+ export { BalanceDisplay, Button, Label, Layout, Modal, Panel, ProgressBar, ScrollContainer, Toast, WinDisplay };
1072
1826
  //# sourceMappingURL=ui.esm.js.map