@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
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) =>
|
|
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
|
-
|
|
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
|