@open-slot-ui/pixi 0.4.2 → 0.5.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/index.cjs CHANGED
@@ -172,15 +172,14 @@ var SpinView = class extends ControlView {
172
172
  this.holdDelayMs = opts.holdDelayMs ?? 260;
173
173
  this.skin = skinFactory(ui.theme);
174
174
  this.art.addChild(this.skin.view);
175
- const c = ui.theme.color;
176
- const ring = new pixi_js.Graphics().circle(0, 0, 64).fill({ color: c.surface }).stroke({ width: 6, color: c.accent });
177
- this.fsCount = new pixi_js.Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 44, fill: c.text, fontWeight: "800" } });
175
+ const fsRing = new pixi_js.Graphics().circle(0, 0, 98).fill({ color: 16777215 }).circle(0, 0, 94).stroke({ width: 8, color: 657930 });
176
+ this.fsCount = new pixi_js.Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 60, fill: 657930, fontWeight: "900" } });
178
177
  this.fsCount.anchor.set(0.5);
179
- this.fsCount.y = -8;
180
- this.fsLabel = new pixi_js.Text({ text: ui.t("openui.freeSpins"), style: { fontFamily: ui.theme.type.family, fontSize: 18, fill: c.accent, fontWeight: "800", letterSpacing: 2 } });
178
+ this.fsCount.y = -14;
179
+ this.fsLabel = new pixi_js.Text({ text: ui.t("openui.freeSpins"), style: { fontFamily: ui.theme.type.family, fontSize: 24, fill: 657930, fontWeight: "900", letterSpacing: 3 } });
181
180
  this.fsLabel.anchor.set(0.5);
182
- this.fsLabel.y = 26;
183
- this.fsFace.addChild(ring, this.fsCount, this.fsLabel);
181
+ this.fsLabel.y = 34;
182
+ this.fsFace.addChild(fsRing, this.fsCount, this.fsLabel);
184
183
  this.fsFace.visible = false;
185
184
  this.art.addChild(this.fsFace);
186
185
  const stopRing = new pixi_js.Graphics().circle(0, 0, 96).fill({ color: 16777215 }).stroke({ width: 7, color: 657930 });
@@ -1220,7 +1219,6 @@ var ValueDisplayView = class extends ControlView {
1220
1219
  }
1221
1220
  build() {
1222
1221
  const theme = this.ui.theme;
1223
- const side = this.side();
1224
1222
  if (this.vd.label) {
1225
1223
  this.caption = new pixi_js.Text({
1226
1224
  // Figma: the label is NOT uppercased — "Баланс" / "Ставка" as authored.
@@ -1228,19 +1226,32 @@ var ValueDisplayView = class extends ControlView {
1228
1226
  style: { fontFamily: theme.type.family, fontSize: 28, fill: 16777215, fontWeight: "900", letterSpacing: 0 }
1229
1227
  });
1230
1228
  this.caption.anchor.set(0.5, 0.5);
1231
- const padX = 24;
1232
- const padY = 8;
1233
- const pillW = Math.ceil(this.caption.width) + padX * 2;
1234
- const pillH = Math.ceil(this.caption.height) + padY * 2;
1235
- const pillX = side === "left" ? 0 : side === "right" ? -pillW : -pillW / 2;
1236
- const pillY = -DIGIT_H / 2 - pillH - 14;
1237
- this.captionPill = new pixi_js.Graphics().roundRect(pillX, pillY, pillW, pillH, 12).fill({ color: 0 });
1238
- this.caption.position.set(pillX + pillW / 2, pillY + pillH / 2);
1229
+ this.captionPill = new pixi_js.Graphics();
1239
1230
  this.addChild(this.captionPill, this.caption);
1231
+ this.sizeCaptionPill();
1232
+ if (typeof document !== "undefined" && document.fonts?.ready) {
1233
+ void document.fonts.ready.then(() => {
1234
+ if (!this.destroyed && this.caption) this.sizeCaptionPill();
1235
+ });
1236
+ }
1240
1237
  }
1241
1238
  const cur = this.vd.currency.get();
1242
1239
  this.buildCounter(core.displayDigits(this.vd.get(), this.vd.digits, cur.decimals), this.vd.minorUnits);
1243
1240
  }
1241
+ /** Size the caption pill to the caption's current measured width + padding and centre
1242
+ * the text in it, anchored to the value's side. Re-runnable (fonts-ready / relayout). */
1243
+ sizeCaptionPill() {
1244
+ if (!this.caption || !this.captionPill) return;
1245
+ const side = this.side();
1246
+ const padX = 24;
1247
+ const padY = 8;
1248
+ const pillW = Math.ceil(this.caption.width) + padX * 2;
1249
+ const pillH = Math.ceil(this.caption.height) + padY * 2;
1250
+ const pillX = side === "left" ? 0 : side === "right" ? -pillW : -pillW / 2;
1251
+ const pillY = -DIGIT_H / 2 - pillH - 14;
1252
+ this.captionPill.clear().roundRect(pillX, pillY, pillW, pillH, 12).fill({ color: 0 });
1253
+ this.caption.position.set(pillX + pillW / 2, pillY + pillH / 2);
1254
+ }
1244
1255
  /** (Re)create the rolling counter sized to `total` columns, starting at `fromMinor`. */
1245
1256
  buildCounter(total, fromMinor) {
1246
1257
  this.counter?.destroy();
@@ -1282,7 +1293,7 @@ var ValueDisplayView = class extends ControlView {
1282
1293
  const symbolMode = cur.display === "symbol" && !!cur.symbol;
1283
1294
  const affix = symbolMode ? cur.symbol : cur.code;
1284
1295
  if ((cur.position ?? "suffix") === "prefix") opts.prefix = symbolMode ? affix : `${affix} `;
1285
- else opts.suffix = symbolMode ? affix : ` ${affix}`;
1296
+ else opts.suffix = ` ${affix}`;
1286
1297
  if (this.fitGsap) {
1287
1298
  opts.fit = {
1288
1299
  gsap: this.fitGsap,
@@ -1297,7 +1308,13 @@ var ValueDisplayView = class extends ControlView {
1297
1308
  if (this.fitGsap) {
1298
1309
  this.counter.x = 0;
1299
1310
  } else {
1300
- this.counter.x = side === "left" ? 0 : side === "right" ? -this.counter.width : -this.counter.width / 2;
1311
+ const budget = core.valueFitMaxWidth(Math.max(this.vd.digits, FIT_BUDGET_COLS), DIGIT_W);
1312
+ const natural = this.counter.width;
1313
+ const k = natural > budget ? Math.max(0.5, budget / natural) : 1;
1314
+ this.counter.scale.set(k);
1315
+ this.counter.y = -DIGIT_H * k / 2;
1316
+ const w = natural * k;
1317
+ this.counter.x = side === "left" ? 0 : side === "right" ? -w : -w / 2;
1301
1318
  }
1302
1319
  this.addChild(this.counter);
1303
1320
  }
@@ -2390,6 +2407,16 @@ function buildBlockColumn(blocks, controls, ui, ticker, bodyW, opts = {}) {
2390
2407
  wrap.addChild(txt);
2391
2408
  return wrap;
2392
2409
  };
2410
+ const hintNode = (s) => {
2411
+ const wrap = new pixi_js.Container();
2412
+ const txt = new pixi_js.Text({
2413
+ text: s,
2414
+ style: { fontFamily: t.type.family, fontSize: 12.5, fill: t.color.textDim, align: "center", wordWrap: true, wordWrapWidth: innerW * 0.86, lineHeight: 17 }
2415
+ });
2416
+ txt.anchor.set(0.5);
2417
+ wrap.addChild(txt);
2418
+ return wrap;
2419
+ };
2393
2420
  const makeView = (b, control) => {
2394
2421
  const skin = opts.controlSkins?.[b.id];
2395
2422
  if (skin) return skin(control, ui, ticker);
@@ -2475,6 +2502,8 @@ function buildBlockColumn(blocks, controls, ui, ticker, bodyW, opts = {}) {
2475
2502
  if (b.kind === "slider") view.position.set(-130, -27);
2476
2503
  wrap.addChild(view);
2477
2504
  placeFixed(wrap, ROW_H);
2505
+ const hint = b.hint;
2506
+ if (hint) placeAuto(hintNode(tr(hint)), 12);
2478
2507
  }
2479
2508
  }
2480
2509
  }
@@ -2985,6 +3014,8 @@ var ReadoutView = class extends ControlView {
2985
3014
  const inline = opts.inline ?? false;
2986
3015
  this.prefix = opts.prefix ?? false;
2987
3016
  const fill = opts.fill ?? (opts.mono ? "#ffffff" : t.color.text);
3017
+ this.baseFill = this.prefix ? opts.fill ?? "#ffffff" : fill;
3018
+ this.accentFill = t.color.accent;
2988
3019
  if (this.prefix) {
2989
3020
  this.valueText = new pixi_js.Text({
2990
3021
  text: "",
@@ -3003,13 +3034,15 @@ var ReadoutView = class extends ControlView {
3003
3034
  this.ro.value.subscribe(() => this.render()),
3004
3035
  this.ui.locale.subscribe(() => {
3005
3036
  if (!this.destroyed) this.render();
3006
- })
3037
+ }),
3038
+ this.ro.emphasized.subscribe(() => this.applyEmphasis())
3007
3039
  );
3008
3040
  if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
3009
3041
  if (this.ro.kind === "duration") {
3010
3042
  this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
3011
3043
  this.ticker.add(this.tick);
3012
3044
  }
3045
+ this.applyEmphasis();
3013
3046
  this.render();
3014
3047
  return;
3015
3048
  }
@@ -3044,10 +3077,12 @@ var ReadoutView = class extends ControlView {
3044
3077
  })
3045
3078
  );
3046
3079
  if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
3080
+ this.disposers.push(this.ro.emphasized.subscribe(() => this.applyEmphasis()));
3047
3081
  if (this.ro.kind === "duration") {
3048
3082
  this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
3049
3083
  this.ticker.add(this.tick);
3050
3084
  }
3085
+ this.applyEmphasis();
3051
3086
  this.render();
3052
3087
  }
3053
3088
  ro;
@@ -3056,6 +3091,14 @@ var ReadoutView = class extends ControlView {
3056
3091
  valueText;
3057
3092
  tick;
3058
3093
  prefix;
3094
+ /** Normal + accent value colours for the `emphasized` (modified) tint. */
3095
+ baseFill;
3096
+ accentFill;
3097
+ /** Tint the value in the theme accent while the readout is emphasized (modified). */
3098
+ applyEmphasis() {
3099
+ if (this.destroyed) return;
3100
+ this.valueText.style.fill = this.ro.emphasized.get() ? this.accentFill : this.baseFill;
3101
+ }
3059
3102
  render() {
3060
3103
  const v = this.ro.value.get();
3061
3104
  let text;
@@ -3291,6 +3334,10 @@ var OpenUIPixi = class {
3291
3334
  slideTickFn;
3292
3335
  slideBaseY = /* @__PURE__ */ new Map();
3293
3336
  slideSign = /* @__PURE__ */ new Map();
3337
+ /** Buy-feature ⇄ total-win swap: only one shows at a time (see `setupBonusSwap`). */
3338
+ bonusView;
3339
+ totalWinView;
3340
+ fadeFns = /* @__PURE__ */ new Map();
3294
3341
  /** The bus event log backing `window.__OPENUI__.events` (available after mount). */
3295
3342
  get eventLog() {
3296
3343
  return this._eventLog;
@@ -3338,6 +3385,9 @@ var OpenUIPixi = class {
3338
3385
  const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 60, iconTexture: ic.bonus, iconTarget: 130 });
3339
3386
  const betPlusView = new ButtonView(this.ui.betPlus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betPlus, iconTarget: 64 });
3340
3387
  const betMinusView = new ButtonView(this.ui.betMinus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betMinus, iconTarget: 64 });
3388
+ const totalWinView = new ValueDisplayView(this.ui.totalWin, this.ui, ticker, this.opts.gsap);
3389
+ this.bonusView = bonusView;
3390
+ this.totalWinView = totalWinView;
3341
3391
  const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "speaker", iconTarget: 60, mono: true });
3342
3392
  const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "fullscreen", iconTarget: 60, mono: true });
3343
3393
  muteView.zIndex = 65;
@@ -3354,6 +3404,7 @@ var OpenUIPixi = class {
3354
3404
  [this.ui.turbo.id, turboView],
3355
3405
  [this.ui.autoplay.id, autoplayView],
3356
3406
  [this.ui.bonusButton.id, bonusView],
3407
+ [this.ui.totalWin.id, totalWinView],
3357
3408
  [this.ui.betPlus.id, betPlusView],
3358
3409
  [this.ui.betMinus.id, betMinusView],
3359
3410
  [this.ui.muteButton.id, muteView],
@@ -3482,9 +3533,81 @@ var OpenUIPixi = class {
3482
3533
  if (intro === "slide-in") this.setControlsVisible(true);
3483
3534
  }
3484
3535
  this.disposers.push(() => renderer.off("resize", onResize), unsubScreen);
3536
+ this.setupBonusSwap();
3485
3537
  this._eventLog = new core.EventLog(this.ui.bus);
3486
3538
  if (this.opts.expose ?? true) this.expose();
3487
3539
  }
3540
+ /**
3541
+ * Wire the buy-feature ⇄ total-win swap. In base play the buy button shows (unless
3542
+ * hidden by config/jurisdiction); once the spin button enters free-spins mode
3543
+ * (`setFreeSpins(n > 0)` — you "can't buy" mid-bonus) the buy button fades out and
3544
+ * the localized total-win counter fades in over the same slot, then back on exit.
3545
+ */
3546
+ setupBonusSwap() {
3547
+ const bonus = this.bonusView;
3548
+ const tw = this.totalWinView;
3549
+ if (!bonus || !tw) return;
3550
+ const apply = (inBonus, animate) => {
3551
+ const bonusShouldShow = !inBonus && !this.ui.hidden.has(this.ui.bonusButton.id);
3552
+ if (animate) {
3553
+ if (inBonus) {
3554
+ tw.visible = true;
3555
+ tw.alpha = 0;
3556
+ this.fadeTo(tw, 1, 220);
3557
+ this.fadeTo(bonus, 0, 160, () => bonus.visible = false);
3558
+ } else {
3559
+ if (bonusShouldShow) {
3560
+ bonus.visible = true;
3561
+ bonus.alpha = 0;
3562
+ this.fadeTo(bonus, this.ui.bonusButton.interactable ? 1 : 0.4, 220);
3563
+ }
3564
+ this.fadeTo(tw, 0, 160, () => tw.visible = false);
3565
+ }
3566
+ } else {
3567
+ this.stopFade(bonus);
3568
+ this.stopFade(tw);
3569
+ bonus.visible = bonusShouldShow;
3570
+ bonus.alpha = this.ui.bonusButton.interactable ? 1 : 0.4;
3571
+ tw.visible = inBonus;
3572
+ tw.alpha = inBonus ? 1 : 0;
3573
+ }
3574
+ };
3575
+ apply(this.ui.spin.freeSpins.get() > 0, false);
3576
+ this.disposers.push(this.ui.spin.freeSpins.subscribe((n) => apply(n > 0, true)));
3577
+ }
3578
+ /** Fade a view's alpha to `to` over `ms` (wall-clock paced); cancels any prior fade
3579
+ * on the same view. `onDone` runs once when it settles. */
3580
+ fadeTo(view, to, ms, onDone) {
3581
+ this.stopFade(view);
3582
+ const t = this.appTicker;
3583
+ const from = view.alpha;
3584
+ if (!t || from === to) {
3585
+ view.alpha = to;
3586
+ onDone?.();
3587
+ return;
3588
+ }
3589
+ const startMs = typeof performance !== "undefined" ? performance.now() : 0;
3590
+ const fn = () => {
3591
+ const nowMs = typeof performance !== "undefined" ? performance.now() : startMs + ms;
3592
+ const k = Math.min(1, (nowMs - startMs) / Math.max(ms, 1));
3593
+ view.alpha = from + (to - from) * k;
3594
+ if (k >= 1) {
3595
+ view.alpha = to;
3596
+ this.stopFade(view);
3597
+ onDone?.();
3598
+ }
3599
+ };
3600
+ this.fadeFns.set(view, fn);
3601
+ t.add(fn);
3602
+ }
3603
+ /** Cancel an in-flight fade on `view` (if any). */
3604
+ stopFade(view) {
3605
+ const fn = this.fadeFns.get(view);
3606
+ if (fn) {
3607
+ this.appTicker?.remove(fn);
3608
+ this.fadeFns.delete(view);
3609
+ }
3610
+ }
3488
3611
  /**
3489
3612
  * Build the unified scrollable MENU bound to the ☰ panel. Composed `menu` blocks
3490
3613
  * are given by `mountHud`; absent → a default Settings-only menu. Built-in ids
@@ -3559,6 +3682,8 @@ var OpenUIPixi = class {
3559
3682
  unmount() {
3560
3683
  if (this.slideTickFn && this.appTicker) this.appTicker.remove(this.slideTickFn);
3561
3684
  this.slideTickFn = void 0;
3685
+ for (const fn of this.fadeFns.values()) this.appTicker?.remove(fn);
3686
+ this.fadeFns.clear();
3562
3687
  for (const v of this.views) v.dispose();
3563
3688
  this.views.length = 0;
3564
3689
  for (const o of this.overlays) o.dispose();
@@ -3904,9 +4029,11 @@ function mountHud(app, spec = {}, opts = {}) {
3904
4029
  on,
3905
4030
  setBalance: (n) => ui.balance.set(n),
3906
4031
  setBet: (n) => ui.bet.set(n),
4032
+ setTotalWin: (n) => ui.totalWin.set(n),
3907
4033
  setCurrency: (c) => {
3908
4034
  ui.balance.setCurrency(c);
3909
4035
  ui.bet.setCurrency(c);
4036
+ ui.totalWin.setCurrency(c);
3910
4037
  ui.netPosition.setCurrency(c);
3911
4038
  },
3912
4039
  applyJurisdiction: (j) => ui.applyJurisdiction(j),