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