@energy8platform/shell 0.10.0 → 0.11.1
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/dist/html.cjs.js +280 -10
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +56 -4
- package/dist/html.esm.js +278 -11
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +81 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +56 -4
- package/dist/index.esm.js +79 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +314 -10
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +56 -4
- package/dist/pixi.esm.js +312 -11
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +3 -0
- package/src/core/keyboard.ts +7 -1
- package/src/core/scrollHint.ts +91 -0
- package/src/core/state.ts +15 -0
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +2 -1
- package/src/ui/html/components/BuyBonus.ts +11 -1
- package/src/ui/html/components/GameInfo.ts +3 -1
- package/src/ui/html/components/pickers.ts +4 -0
- package/src/ui/html/primitives.ts +16 -5
- package/src/ui/html/scroll-affordance.ts +144 -0
- package/src/ui/html/shell.css.ts +65 -1
- package/src/ui/pixi/components/BottomBar.ts +9 -3
- package/src/ui/pixi/components/BuyBonus.ts +20 -1
- package/src/ui/pixi/primitives/overlay.ts +1 -1
- package/src/ui/pixi/primitives/scroll-affordance.ts +244 -0
- package/src/ui/pixi/primitives/scroll.ts +21 -3
package/dist/pixi.cjs.js
CHANGED
|
@@ -325,6 +325,20 @@ function nextTurbo(current, maxLevels) {
|
|
|
325
325
|
return 0;
|
|
326
326
|
return current >= maxLevels ? 0 : current + 1;
|
|
327
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Is a bonus buy unavailable right now?
|
|
330
|
+
*
|
|
331
|
+
* The three RUNTIME locks, in one place because they used to be in three: both bottom bars spelled
|
|
332
|
+
* them out for the coin, and the Shift+B hotkey spelled out a different, shorter set — so the
|
|
333
|
+
* keyboard opened the buy-bonus overlay mid-round and let a player stake a second bet on top of a
|
|
334
|
+
* round already in flight. A predicate the bars and the hotkey share cannot drift apart again.
|
|
335
|
+
*
|
|
336
|
+
* Runtime only. Whether the feature EXISTS at all is a config question (`features.buyBonus`), and
|
|
337
|
+
* whether this particular surface should offer it (mode, replay) belongs to the caller.
|
|
338
|
+
*/
|
|
339
|
+
function bonusBuyLocked(s) {
|
|
340
|
+
return s.busy || s.autoplay.active || !s.buyBonusEnabled;
|
|
341
|
+
}
|
|
328
342
|
|
|
329
343
|
/** The single brand accent (purple). The bar accent default (theme.ts) and the buy-a-bonus
|
|
330
344
|
* card default both derive from this, so a rebrand only touches one constant. */
|
|
@@ -1627,7 +1641,9 @@ class KeyboardController {
|
|
|
1627
1641
|
}
|
|
1628
1642
|
break;
|
|
1629
1643
|
case 'KeyB':
|
|
1630
|
-
|
|
1644
|
+
// `bonusBuyLocked` is the same predicate the bar's coin uses. Without it this hotkey
|
|
1645
|
+
// reached past a disabled coin and opened the overlay mid-round.
|
|
1646
|
+
if (h.buyBonusEnabled && s.mode === 'base' && !s.replay && !bonusBuyLocked(s)) {
|
|
1631
1647
|
h.openBuyBonus();
|
|
1632
1648
|
return;
|
|
1633
1649
|
}
|
|
@@ -1739,7 +1755,7 @@ function keyboardCapable(win = typeof window === 'undefined' ? undefined : windo
|
|
|
1739
1755
|
|
|
1740
1756
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1741
1757
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1742
|
-
const PACKAGE_VERSION = '0.
|
|
1758
|
+
const PACKAGE_VERSION = '0.11.1';
|
|
1743
1759
|
|
|
1744
1760
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1745
1761
|
function resolveConfig(config) {
|
|
@@ -2225,6 +2241,66 @@ function placePopover(anchor, surface, size, pointer = null) {
|
|
|
2225
2241
|
return { x, y, maxH, arrowX, below };
|
|
2226
2242
|
}
|
|
2227
2243
|
|
|
2244
|
+
/**
|
|
2245
|
+
* What a scrollable region should ADVERTISE about itself.
|
|
2246
|
+
*
|
|
2247
|
+
* A slot in a Stake popout is 400×225. At that size the game-info overlay holds twelve screens of
|
|
2248
|
+
* content, the buy-bonus switches to a vertical card stack, and the menu popover hides its last
|
|
2249
|
+
* row — all of them scroll, and (before this module) none of them said so. macOS makes it worse:
|
|
2250
|
+
* overlay scrollbars stay invisible until something is already scrolling, so the very affordance a
|
|
2251
|
+
* player needs BEFORE they touch anything is the one the OS withholds. A certification reviewer
|
|
2252
|
+
* reads that as content the player can't reach.
|
|
2253
|
+
*
|
|
2254
|
+
* The maths lives here, apart from both renderers, for one reason: the DOM shell reads
|
|
2255
|
+
* `scrollTop`/`scrollHeight` while the Pixi shell tracks its own offset against a mask, and those
|
|
2256
|
+
* two must never disagree about whether a fade belongs at the bottom edge. Renderers decide how a
|
|
2257
|
+
* thumb LOOKS; this decides when there is one and where it sits.
|
|
2258
|
+
*/
|
|
2259
|
+
/** Fractions of a pixel are layout rounding, not reachable content. */
|
|
2260
|
+
const EPSILON = 1;
|
|
2261
|
+
/** Landing within half a pixel of an edge counts as arriving: a browser settles a flung scroll on
|
|
2262
|
+
* 199.5 of 200, and a fade left glowing over content the player has already reached reads as a
|
|
2263
|
+
* bug. */
|
|
2264
|
+
const EDGE_EPSILON = 0.5;
|
|
2265
|
+
/**
|
|
2266
|
+
* Shortest thumb we will draw, as a fraction of its track.
|
|
2267
|
+
*
|
|
2268
|
+
* The honest ratio for game info at Popout S is 8% — an 18px speck on a 226px track, which reads
|
|
2269
|
+
* as a rendering artifact rather than a scrollbar. Floored at 18% it stays recognisably a thumb,
|
|
2270
|
+
* and it still travels the whole track, so the position it reports remains truthful even though
|
|
2271
|
+
* its length no longer is. That is the right trade: length is decoration, position is information.
|
|
2272
|
+
*/
|
|
2273
|
+
const SCROLL_THUMB_MIN = 0.18;
|
|
2274
|
+
/** Resolve one axis of a scroll region into everything a renderer needs to draw its affordance. */
|
|
2275
|
+
function scrollHint(m) {
|
|
2276
|
+
const view = Math.max(0, m.clientHeight);
|
|
2277
|
+
const content = Math.max(0, m.scrollHeight);
|
|
2278
|
+
const maxScroll = Math.max(0, content - view);
|
|
2279
|
+
if (maxScroll <= EPSILON || view <= 0) {
|
|
2280
|
+
// Both edges are "the edge" when there is nowhere to go — callers gate every fade on the
|
|
2281
|
+
// matching flag, so a region that fits draws nothing without needing to check `overflowing`.
|
|
2282
|
+
return { overflowing: false, atStart: true, atEnd: true, thumbSize: 1, thumbOffset: 0, maxScroll: 0 };
|
|
2283
|
+
}
|
|
2284
|
+
const at = Math.max(0, Math.min(maxScroll, m.scrollTop));
|
|
2285
|
+
const thumbSize = Math.min(1, Math.max(SCROLL_THUMB_MIN, view / content));
|
|
2286
|
+
const progress = at / maxScroll;
|
|
2287
|
+
return {
|
|
2288
|
+
overflowing: true,
|
|
2289
|
+
atStart: at <= EDGE_EPSILON,
|
|
2290
|
+
atEnd: maxScroll - at <= EDGE_EPSILON,
|
|
2291
|
+
thumbSize,
|
|
2292
|
+
thumbOffset: progress * (1 - thumbSize),
|
|
2293
|
+
maxScroll,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function scrollEdge(h) {
|
|
2297
|
+
if (!h.overflowing)
|
|
2298
|
+
return 'none';
|
|
2299
|
+
if (h.atStart)
|
|
2300
|
+
return 'start';
|
|
2301
|
+
return h.atEnd ? 'end' : 'mid';
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2228
2304
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2229
2305
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
2230
2306
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -3869,7 +3945,12 @@ class BottomBar extends pixi_js.Container {
|
|
|
3869
3945
|
if (config.features.turbo > 0) {
|
|
3870
3946
|
this.turboBtn = makeTurboButton(this.host, state.turbo, () => this.onTurbo(), 40, 22);
|
|
3871
3947
|
}
|
|
3872
|
-
|
|
3948
|
+
// On `this`, not a local: applyBusy() guards every line with `if (this.<control>)`, so a coin
|
|
3949
|
+
// kept in a local is drawn, laid out, tappable — and unreachable by every lock there is.
|
|
3950
|
+
// That is exactly how a phone player could tap SPIN and open buy-bonus on the round in
|
|
3951
|
+
// flight. (Safe for layout: applyFit() hands mobile to applyFitMobile() before it touches
|
|
3952
|
+
// `this.buy` for the wide bar's positioning.)
|
|
3953
|
+
this.buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
|
|
3873
3954
|
// level 1 — controls bar (dark)
|
|
3874
3955
|
const controls = new FlexBox({
|
|
3875
3956
|
direction: 'row',
|
|
@@ -3890,8 +3971,8 @@ class BottomBar extends pixi_js.Container {
|
|
|
3890
3971
|
const rz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'end' });
|
|
3891
3972
|
if (this.turboBtn)
|
|
3892
3973
|
rz.add(this.turboBtn);
|
|
3893
|
-
if (buy)
|
|
3894
|
-
rz.add(buy);
|
|
3974
|
+
if (this.buy)
|
|
3975
|
+
rz.add(this.buy);
|
|
3895
3976
|
const zw = Math.max(lz.measureSize().w, rz.measureSize().w);
|
|
3896
3977
|
lz.setLayoutSize(zw, undefined);
|
|
3897
3978
|
rz.setLayoutSize(zw, undefined);
|
|
@@ -4057,7 +4138,7 @@ class BottomBar extends pixi_js.Container {
|
|
|
4057
4138
|
if (this.autoBtn)
|
|
4058
4139
|
this.autoBtn.disabled = state.busy && !auto;
|
|
4059
4140
|
if (this.buy)
|
|
4060
|
-
this.buy.disabled = state
|
|
4141
|
+
this.buy.disabled = bonusBuyLocked(state);
|
|
4061
4142
|
if (this.betReadout && lockBet) {
|
|
4062
4143
|
this.betReadout.eventMode = 'none';
|
|
4063
4144
|
this.betReadout.cursor = 'default';
|
|
@@ -4150,11 +4231,207 @@ function SPIN_POP_MOBILE() {
|
|
|
4150
4231
|
return (SPIN - M_CTRL_H) / 2; // 11
|
|
4151
4232
|
}
|
|
4152
4233
|
|
|
4234
|
+
/** Thickness of the thumb, and how far it is inset from the edge it rides. */
|
|
4235
|
+
const THUMB_THICK = 4;
|
|
4236
|
+
const THUMB_INSET = 3;
|
|
4237
|
+
/** Shortest thumb we will draw in pixels — the fractional floor still degenerates on a tiny frame. */
|
|
4238
|
+
const THUMB_MIN_PX = 22;
|
|
4239
|
+
/** Depth of the edge scrim. */
|
|
4240
|
+
const SCRIM = 34;
|
|
4241
|
+
const SCRIM_ALPHA = 0.72;
|
|
4242
|
+
/** The overlay chrome — plaques, bar, backdrop — is scheme-independent in this shell (see
|
|
4243
|
+
* resolveTheme: those tokens are fixed, only the palette behind them flips). The affordance is
|
|
4244
|
+
* chrome, so it is a fixed neutral too: white marks over a tint of the same hue as the veil, which
|
|
4245
|
+
* makes the fade read as content dissolving INTO the overlay rather than as a grey band on top. */
|
|
4246
|
+
const SCRIM_TINT = 0x0c111c;
|
|
4247
|
+
const CUE_R = 11;
|
|
4248
|
+
const CUE_BOB = 4;
|
|
4249
|
+
const CUE_PERIOD = 1600; // ms
|
|
4250
|
+
/**
|
|
4251
|
+
* The scrim's alpha ramp, densest AT the edge and clear by `depth` inward.
|
|
4252
|
+
*
|
|
4253
|
+
* Exported because the direction is the whole point and is easy to get backwards: an inverted ramp
|
|
4254
|
+
* draws a hard-edged dark band floating over the content instead of a fade into the frame, and it
|
|
4255
|
+
* looks deliberate enough that nobody questions it. The squared falloff keeps the outer half of the
|
|
4256
|
+
* ramp faint, so the scrim reads as content thinning out rather than as a panel laid on top.
|
|
4257
|
+
*/
|
|
4258
|
+
function scrimRamp(depth) {
|
|
4259
|
+
const slices = Math.max(1, Math.round(depth));
|
|
4260
|
+
const thickness = depth / slices;
|
|
4261
|
+
const out = [];
|
|
4262
|
+
for (let i = 0; i < slices; i++) {
|
|
4263
|
+
const offset = i * thickness;
|
|
4264
|
+
const t = 1 - (offset + thickness / 2) / depth; // 1 at the edge → 0 at `depth`
|
|
4265
|
+
out.push({ offset, thickness, alpha: SCRIM_ALPHA * t * t });
|
|
4266
|
+
}
|
|
4267
|
+
return out;
|
|
4268
|
+
}
|
|
4269
|
+
class ScrollAffordanceView extends pixi_js.Container {
|
|
4270
|
+
thumb = new pixi_js.Graphics();
|
|
4271
|
+
scrim = new pixi_js.Graphics();
|
|
4272
|
+
cue = new pixi_js.Container();
|
|
4273
|
+
ticker;
|
|
4274
|
+
bounds = null;
|
|
4275
|
+
cueOn = false;
|
|
4276
|
+
// Once the region has moved the player knows the gesture; re-offering it on every return to the
|
|
4277
|
+
// top would nag rather than inform.
|
|
4278
|
+
cueRetired = false;
|
|
4279
|
+
cueBaseY = 0;
|
|
4280
|
+
elapsed = 0;
|
|
4281
|
+
tick;
|
|
4282
|
+
constructor(ticker) {
|
|
4283
|
+
super();
|
|
4284
|
+
this.ticker = ticker;
|
|
4285
|
+
this.eventMode = 'none'; // decoration: never intercept the drag it is advertising
|
|
4286
|
+
this.addChild(this.scrim, this.thumb, this.cue);
|
|
4287
|
+
this.buildCue();
|
|
4288
|
+
this.visible = false;
|
|
4289
|
+
}
|
|
4290
|
+
/** Where the thumb is drawn, in the parent's space — `null` when nothing is drawn. */
|
|
4291
|
+
get thumbBounds() {
|
|
4292
|
+
return this.visible ? this.bounds : null;
|
|
4293
|
+
}
|
|
4294
|
+
get cueVisible() {
|
|
4295
|
+
return this.visible && this.cueOn;
|
|
4296
|
+
}
|
|
4297
|
+
/** Re-draw for a viewport and a hint. Cheap enough to call on every scroll frame. */
|
|
4298
|
+
update(vp, hint) {
|
|
4299
|
+
if (this.destroyed)
|
|
4300
|
+
return;
|
|
4301
|
+
if (!hint.overflowing) {
|
|
4302
|
+
this.visible = false;
|
|
4303
|
+
this.bounds = null;
|
|
4304
|
+
this.showCue(false);
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
this.visible = true;
|
|
4308
|
+
// Any movement off the start retires the cue permanently.
|
|
4309
|
+
if (!hint.atStart)
|
|
4310
|
+
this.cueRetired = true;
|
|
4311
|
+
this.drawThumb(vp, hint);
|
|
4312
|
+
this.drawScrim(vp, hint);
|
|
4313
|
+
this.placeCue(vp);
|
|
4314
|
+
this.showCue(hint.atStart && !this.cueRetired);
|
|
4315
|
+
}
|
|
4316
|
+
drawThumb(vp, hint) {
|
|
4317
|
+
const along = vp.axis === 'y' ? vp.h : vp.w;
|
|
4318
|
+
const track = Math.max(0, along - THUMB_INSET * 2);
|
|
4319
|
+
const len = Math.max(Math.min(track, THUMB_MIN_PX), track * hint.thumbSize);
|
|
4320
|
+
// Recover the travel from the (possibly floored) length so the thumb still reaches both ends.
|
|
4321
|
+
const travel = Math.max(0, track - len);
|
|
4322
|
+
const progress = hint.thumbSize < 1 ? hint.thumbOffset / (1 - hint.thumbSize) : 0;
|
|
4323
|
+
const at = THUMB_INSET + travel * progress;
|
|
4324
|
+
this.bounds =
|
|
4325
|
+
vp.axis === 'y'
|
|
4326
|
+
? { x: vp.x + vp.w - THUMB_INSET - THUMB_THICK, y: vp.y + at, w: THUMB_THICK, h: len }
|
|
4327
|
+
: { x: vp.x + at, y: vp.y + vp.h - THUMB_INSET - THUMB_THICK, w: len, h: THUMB_THICK };
|
|
4328
|
+
const b = this.bounds;
|
|
4329
|
+
this.thumb.clear();
|
|
4330
|
+
// A faint track behind the thumb: on a dark overlay a lone thumb can read as a stray highlight;
|
|
4331
|
+
// with the groove behind it, it reads as a scrollbar.
|
|
4332
|
+
if (vp.axis === 'y') {
|
|
4333
|
+
this.thumb
|
|
4334
|
+
.roundRect(b.x, vp.y + THUMB_INSET, THUMB_THICK, track, THUMB_THICK / 2)
|
|
4335
|
+
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
4336
|
+
}
|
|
4337
|
+
else {
|
|
4338
|
+
this.thumb
|
|
4339
|
+
.roundRect(vp.x + THUMB_INSET, b.y, track, THUMB_THICK, THUMB_THICK / 2)
|
|
4340
|
+
.fill({ color: 0xffffff, alpha: 0.1 });
|
|
4341
|
+
}
|
|
4342
|
+
this.thumb.roundRect(b.x, b.y, b.w, b.h, THUMB_THICK / 2).fill({ color: 0xffffff, alpha: 0.55 });
|
|
4343
|
+
}
|
|
4344
|
+
/** Stacked 1px slices rather than a gradient fill: no texture, no backend-specific paths, and at
|
|
4345
|
+
* one device pixel per slice there is nothing left to band. */
|
|
4346
|
+
drawScrim(vp, hint) {
|
|
4347
|
+
this.scrim.clear();
|
|
4348
|
+
const depth = Math.min(SCRIM, (vp.axis === 'y' ? vp.h : vp.w) / 3);
|
|
4349
|
+
if (depth <= 0)
|
|
4350
|
+
return;
|
|
4351
|
+
for (const { offset, thickness, alpha } of scrimRamp(depth)) {
|
|
4352
|
+
// `offset` is measured INWARD from whichever edge is being faded.
|
|
4353
|
+
if (!hint.atEnd) {
|
|
4354
|
+
if (vp.axis === 'y')
|
|
4355
|
+
this.scrim.rect(vp.x, vp.y + vp.h - offset - thickness, vp.w, thickness);
|
|
4356
|
+
else
|
|
4357
|
+
this.scrim.rect(vp.x + vp.w - offset - thickness, vp.y, thickness, vp.h);
|
|
4358
|
+
this.scrim.fill({ color: SCRIM_TINT, alpha });
|
|
4359
|
+
}
|
|
4360
|
+
if (!hint.atStart) {
|
|
4361
|
+
if (vp.axis === 'y')
|
|
4362
|
+
this.scrim.rect(vp.x, vp.y + offset, vp.w, thickness);
|
|
4363
|
+
else
|
|
4364
|
+
this.scrim.rect(vp.x + offset, vp.y, thickness, vp.h);
|
|
4365
|
+
this.scrim.fill({ color: SCRIM_TINT, alpha });
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
buildCue() {
|
|
4370
|
+
const disc = new pixi_js.Graphics();
|
|
4371
|
+
disc.circle(0, 0, CUE_R).fill({ color: 0x0b0f18, alpha: 0.9 });
|
|
4372
|
+
disc.circle(0, 0, CUE_R).stroke({ color: 0xffffff, alpha: 0.22, width: 1 });
|
|
4373
|
+
const size = Math.round(CUE_R * 1.3);
|
|
4374
|
+
const glyph = makeIcon('chevronDown', size, '#ffffff');
|
|
4375
|
+
glyph.position.set(-size / 2, -size / 2);
|
|
4376
|
+
this.cue.addChild(disc, glyph);
|
|
4377
|
+
this.cue.visible = false;
|
|
4378
|
+
}
|
|
4379
|
+
placeCue(vp) {
|
|
4380
|
+
// Bottom-centre for a vertical region; hugging the trailing edge for a horizontal one.
|
|
4381
|
+
if (vp.axis === 'y') {
|
|
4382
|
+
this.cue.x = vp.x + vp.w / 2;
|
|
4383
|
+
this.cueBaseY = vp.y + vp.h - CUE_R - 6;
|
|
4384
|
+
}
|
|
4385
|
+
else {
|
|
4386
|
+
this.cue.x = vp.x + vp.w - CUE_R - 6;
|
|
4387
|
+
this.cueBaseY = vp.y + vp.h / 2;
|
|
4388
|
+
this.cue.rotation = -Math.PI / 2; // chevron points the way the strip moves
|
|
4389
|
+
}
|
|
4390
|
+
this.cue.y = this.cueBaseY;
|
|
4391
|
+
}
|
|
4392
|
+
showCue(on) {
|
|
4393
|
+
if (on === this.cueOn)
|
|
4394
|
+
return;
|
|
4395
|
+
this.cueOn = on;
|
|
4396
|
+
this.cue.visible = on;
|
|
4397
|
+
if (on)
|
|
4398
|
+
this.startBob();
|
|
4399
|
+
else
|
|
4400
|
+
this.stopBob();
|
|
4401
|
+
}
|
|
4402
|
+
startBob() {
|
|
4403
|
+
// A player who asked for stillness still needs the hint; they just do not need it moving.
|
|
4404
|
+
if (this.tick || !this.ticker || prefersReducedMotion())
|
|
4405
|
+
return;
|
|
4406
|
+
this.elapsed = 0;
|
|
4407
|
+
this.tick = (t) => {
|
|
4408
|
+
this.elapsed += t.deltaMS;
|
|
4409
|
+
const phase = (this.elapsed % CUE_PERIOD) / CUE_PERIOD;
|
|
4410
|
+
this.cue.y = this.cueBaseY + Math.sin(phase * Math.PI * 2) * (CUE_BOB / 2) + CUE_BOB / 2;
|
|
4411
|
+
};
|
|
4412
|
+
this.ticker.add(this.tick);
|
|
4413
|
+
}
|
|
4414
|
+
stopBob() {
|
|
4415
|
+
if (!this.tick)
|
|
4416
|
+
return;
|
|
4417
|
+
this.ticker?.remove(this.tick);
|
|
4418
|
+
this.tick = undefined;
|
|
4419
|
+
this.cue.y = this.cueBaseY;
|
|
4420
|
+
}
|
|
4421
|
+
destroy(options) {
|
|
4422
|
+
this.stopBob();
|
|
4423
|
+
super.destroy(options);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4153
4427
|
/** A vertically-scrolling viewport — mask + drag + mouse-wheel, the Pixi analogue of the
|
|
4154
4428
|
* overlay's `overflow-y:auto` scroll region. Add content to `.content`; call `refresh()`
|
|
4155
4429
|
* after content changes or a resize. */
|
|
4156
4430
|
class ScrollBox extends pixi_js.Container {
|
|
4157
4431
|
content = new pixi_js.Container();
|
|
4432
|
+
/** The drawn "this scrolls" marks — thumb, edge scrim, chevron. Public so the overlay that owns
|
|
4433
|
+
* the box (and the tests) can read what a player would see. */
|
|
4434
|
+
affordance;
|
|
4158
4435
|
maskG = new pixi_js.Graphics();
|
|
4159
4436
|
viewW = 0;
|
|
4160
4437
|
viewH = 0;
|
|
@@ -4165,10 +4442,13 @@ class ScrollBox extends pixi_js.Container {
|
|
|
4165
4442
|
moved = false;
|
|
4166
4443
|
canvas;
|
|
4167
4444
|
wheelHandler;
|
|
4168
|
-
constructor(canvas) {
|
|
4445
|
+
constructor(canvas, ticker) {
|
|
4169
4446
|
super();
|
|
4170
4447
|
this.canvas = canvas;
|
|
4171
|
-
this.
|
|
4448
|
+
this.affordance = new ScrollAffordanceView(ticker);
|
|
4449
|
+
// Added AFTER content, and never masked: the marks describe the viewport, so clipping them to
|
|
4450
|
+
// the content they describe would hide them exactly when the content overflows.
|
|
4451
|
+
this.addChild(this.content, this.affordance);
|
|
4172
4452
|
// maskG is added to the scene only while scrolling (see refresh) — a leftover unused mask
|
|
4173
4453
|
// graphic renders as a white rect. (Masking does NOT gate pointer events to the content, despite
|
|
4174
4454
|
// what an earlier version of this comment claimed — see the correction in refresh() below.)
|
|
@@ -4223,6 +4503,7 @@ class ScrollBox extends pixi_js.Container {
|
|
|
4223
4503
|
}
|
|
4224
4504
|
this.eventMode = scrollable ? 'static' : 'passive';
|
|
4225
4505
|
this.setScroll(this.scrollY);
|
|
4506
|
+
this.syncAffordance();
|
|
4226
4507
|
}
|
|
4227
4508
|
/** Max scrollable distance (0 when content fits) — exposed for tests. */
|
|
4228
4509
|
get maxScrollY() {
|
|
@@ -4239,6 +4520,12 @@ class ScrollBox extends pixi_js.Container {
|
|
|
4239
4520
|
return;
|
|
4240
4521
|
this.scrollY = Math.max(0, Math.min(this.maxScroll, y)) || 0; // || 0 converts -0 to 0
|
|
4241
4522
|
this.content.y = this.scrollY === 0 ? 0 : -this.scrollY;
|
|
4523
|
+
this.syncAffordance();
|
|
4524
|
+
}
|
|
4525
|
+
syncAffordance() {
|
|
4526
|
+
if (this.destroyed || this.affordance.destroyed)
|
|
4527
|
+
return;
|
|
4528
|
+
this.affordance.update({ x: 0, y: 0, w: this.viewW, h: this.viewH, axis: 'y' }, scrollHint({ scrollTop: this.scrollY, scrollHeight: this.viewH + this.maxScroll, clientHeight: this.viewH }));
|
|
4242
4529
|
}
|
|
4243
4530
|
onDown = (e) => {
|
|
4244
4531
|
this.dragging = true;
|
|
@@ -4809,7 +5096,7 @@ class Overlay extends pixi_js.Container {
|
|
|
4809
5096
|
this.host = host;
|
|
4810
5097
|
this.opts = opts;
|
|
4811
5098
|
this.tag = opts.tag;
|
|
4812
|
-
this.scroll = new ScrollBox(host.canvas);
|
|
5099
|
+
this.scroll = new ScrollBox(host.canvas, host.ticker);
|
|
4813
5100
|
this.addChild(this.veil, this.scroll, this.header);
|
|
4814
5101
|
this.veil.eventMode = 'static'; // swallow clicks to the game behind
|
|
4815
5102
|
this.resize(host.screenW, host.screenH);
|
|
@@ -5532,6 +5819,10 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5532
5819
|
header = new pixi_js.Container();
|
|
5533
5820
|
strip = new pixi_js.Container(); // cards live here (drag-scrollable when wide)
|
|
5534
5821
|
stripMask = new pixi_js.Graphics();
|
|
5822
|
+
/** "the strip moves" — the drawn marks over the card band. Unmasked and outside `strip`, so a
|
|
5823
|
+
* drag moves the cards under it while the marks hold still. Public for the same reason
|
|
5824
|
+
* ScrollBox.affordance is: it is the only readable account of what a player sees here. */
|
|
5825
|
+
scrollCue;
|
|
5535
5826
|
footer = new pixi_js.Container();
|
|
5536
5827
|
confirm;
|
|
5537
5828
|
/** The bonus shown in the current confirm dialog (set by openConfirm, cleared by removeConfirm). */
|
|
@@ -5568,7 +5859,8 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5568
5859
|
super();
|
|
5569
5860
|
this.host = host;
|
|
5570
5861
|
this.bonuses = bonuses;
|
|
5571
|
-
this.
|
|
5862
|
+
this.scrollCue = new ScrollAffordanceView(host.ticker);
|
|
5863
|
+
this.addChild(this.veil, this.strip, this.scrollCue, this.header, this.footer);
|
|
5572
5864
|
this.veil.eventMode = 'static';
|
|
5573
5865
|
this.strip.mask = this.stripMask;
|
|
5574
5866
|
this.addChild(this.stripMask);
|
|
@@ -5601,6 +5893,7 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5601
5893
|
this.strip.position.x = this.dragBaseX + this.dragX;
|
|
5602
5894
|
else
|
|
5603
5895
|
this.strip.position.y = this.dragBaseY + this.dragX;
|
|
5896
|
+
this.syncScrollCue();
|
|
5604
5897
|
};
|
|
5605
5898
|
onDragUp = () => {
|
|
5606
5899
|
this.dragging = false;
|
|
@@ -5618,6 +5911,14 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5618
5911
|
this.dragging = false;
|
|
5619
5912
|
this.strip.eventMode = 'auto'; // visual-only; its children (cards) are still hit-tested in-band
|
|
5620
5913
|
this.strip.position.set(baseX, baseY);
|
|
5914
|
+
this.syncScrollCue();
|
|
5915
|
+
}
|
|
5916
|
+
/** Re-draw the band's scroll marks from the current drag state. `dragX` runs [-dragMax, 0] as the
|
|
5917
|
+
* strip is pulled, so the scroll position is its negation. */
|
|
5918
|
+
syncScrollCue() {
|
|
5919
|
+
if (this.destroyed || this.scrollCue.destroyed)
|
|
5920
|
+
return;
|
|
5921
|
+
this.scrollCue.update({ x: 0, y: this.bandTop, w: this.w, h: this.bandH, axis: this.dragAxis }, scrollHint({ scrollTop: -this.dragX, scrollHeight: this.bandH + this.dragMax, clientHeight: this.bandH }));
|
|
5621
5922
|
}
|
|
5622
5923
|
resize(w, h) {
|
|
5623
5924
|
this.w = w;
|
|
@@ -7205,6 +7506,7 @@ exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
|
|
7205
7506
|
exports.POPOVER = POPOVER;
|
|
7206
7507
|
exports.PixiRenderer = PixiRenderer;
|
|
7207
7508
|
exports.SCHEMES = SCHEMES;
|
|
7509
|
+
exports.SCROLL_THUMB_MIN = SCROLL_THUMB_MIN;
|
|
7208
7510
|
exports.ShellController = ShellController;
|
|
7209
7511
|
exports.createI18n = createI18n;
|
|
7210
7512
|
exports.createPixiShell = createPixiShell;
|
|
@@ -7218,6 +7520,8 @@ exports.removePixiShell = removePixiShell;
|
|
|
7218
7520
|
exports.resolveConfig = resolveConfig;
|
|
7219
7521
|
exports.resolveMenu = resolveMenu;
|
|
7220
7522
|
exports.resolveTheme = resolveTheme;
|
|
7523
|
+
exports.scrollEdge = scrollEdge;
|
|
7524
|
+
exports.scrollHint = scrollHint;
|
|
7221
7525
|
exports.seedMenuValues = seedMenuValues;
|
|
7222
7526
|
exports.socialize = socialize;
|
|
7223
7527
|
//# sourceMappingURL=pixi.cjs.js.map
|