@energy8platform/shell 0.6.1 → 0.6.3
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 +74 -62
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +2 -2
- package/dist/html.esm.js +74 -62
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/pixi.cjs.js +159 -174
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +2 -2
- package/dist/pixi.esm.js +160 -175
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/version.ts +1 -1
- package/src/ui/buy-bonus-art.ts +7 -0
- package/src/ui/html/components/BottomBar.ts +19 -14
- package/src/ui/html/components/GameInfo.ts +5 -3
- package/src/ui/html/icons-preview.png +0 -0
- package/src/ui/html/icons-preview.svg +64 -102
- package/src/ui/html/icons.ts +22 -18
- package/src/ui/html/shell.css.ts +22 -23
- package/src/ui/pixi/components/BottomBar.ts +45 -112
- package/src/ui/pixi/components/BuyBonus.ts +4 -4
- package/src/ui/pixi/components/GameInfo.ts +21 -4
- package/src/ui/pixi/icons.ts +22 -26
- package/src/ui/pixi/pixi-icon.ts +16 -41
- package/src/ui/pixi/primitives/widgets.ts +60 -11
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -28,20 +28,13 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
28
28
|
#${SHELL_ROOT_ID} .ge-iconbtn:active { transform:scale(.92); }
|
|
29
29
|
#${SHELL_ROOT_ID} .ge-iconbtn[disabled] { opacity:.35; cursor:default; }
|
|
30
30
|
#${SHELL_ROOT_ID} .ge-iconbtn.ge-active { color:var(--shell-icon-active); }
|
|
31
|
-
/* Turbo
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
standard bar-button behaviour (line ~400: white disc + accent) — the bolt + ring tint accent. */
|
|
39
|
-
#${SHELL_ROOT_ID} .ge-iconbtn[data-ge="turbo"] svg { overflow:visible; }
|
|
40
|
-
#${SHELL_ROOT_ID} .ge-iconbtn[data-ge="turbo"] svg path { paint-order:stroke; stroke:var(--shell-accent); stroke-width:4; stroke-linejoin:round; }
|
|
41
|
-
#${SHELL_ROOT_ID} .ge-iconbtn.ge-turbo-0 svg path { fill:#fff; stroke:#000; }
|
|
42
|
-
#${SHELL_ROOT_ID} .ge-iconbtn.ge-turbo-1 svg path { fill:#fff; }
|
|
43
|
-
#${SHELL_ROOT_ID} .ge-iconbtn.ge-turbo-2 svg path { fill:var(--shell-accent); }
|
|
44
|
-
#${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn[data-ge="turbo"]:hover svg path { fill:var(--shell-accent); stroke:var(--shell-accent); }
|
|
31
|
+
/* Turbo shows its level by SWAPPING the glyph (off → single bolt turboOff, L1 → single bolt
|
|
32
|
+
turbo1, L2 → bolt-with-speed-lines turbo2). Colour just marks state — no outline:
|
|
33
|
+
off (ge-turbo-0) = #ccc (the svg turbo-off grey) · engaged (L1/L2) = accent · hover = accent. */
|
|
34
|
+
#${SHELL_ROOT_ID} .ge-iconbtn[data-ge="turbo"].ge-turbo-0 { color:#ccc; }
|
|
35
|
+
#${SHELL_ROOT_ID} .ge-iconbtn[data-ge="turbo"].ge-turbo-1,
|
|
36
|
+
#${SHELL_ROOT_ID} .ge-iconbtn[data-ge="turbo"].ge-turbo-2 { color:var(--shell-accent); }
|
|
37
|
+
#${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn[data-ge="turbo"]:hover { color:var(--shell-accent); }
|
|
45
38
|
|
|
46
39
|
/* SPIN — white disc by default, big glyph reaching for the rim; lights up accent on hover */
|
|
47
40
|
#${SHELL_ROOT_ID} .ge-shell-spin { pointer-events:auto; cursor:pointer; border:3px solid #000; border-radius:50%;
|
|
@@ -61,8 +54,9 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
61
54
|
#${SHELL_ROOT_ID} .ge-spin-stop { display:flex; } /* STOP glyph at the disc's full size, like SPIN */
|
|
62
55
|
/* no entrance animation: the bar re-renders many times per spin, so any animation here
|
|
63
56
|
would replay each time and make the counter visibly jump. tabular-nums keeps it steady. */
|
|
57
|
+
/* the STOP glyph is a solid dark square, so the autoplay count is always pure white to read on it. */
|
|
64
58
|
#${SHELL_ROOT_ID} .ge-spin-count { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
65
|
-
font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; }
|
|
59
|
+
font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; color:#fff; }
|
|
66
60
|
|
|
67
61
|
/* BUY BONUS — round accent badge, 2-line label, text pulses + accent glow on hover */
|
|
68
62
|
#${SHELL_ROOT_ID} .ge-shell-buybonus { pointer-events:auto; cursor:pointer; box-sizing:border-box;
|
|
@@ -72,7 +66,14 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
72
66
|
#${SHELL_ROOT_ID} .ge-shell-buybonus span { display:inline-block; will-change:transform; }
|
|
73
67
|
/* the ticket glyph fills the badge (em-sized off the button's font-size, so it scales per context) */
|
|
74
68
|
#${SHELL_ROOT_ID} .ge-shell-buybonus .ge-bb-tk { display:flex; font-size:3.5em; color:#0b0e16; }
|
|
69
|
+
/* coin variant (BUY state): the full-colour coin IS the button — drop the accent disc + border,
|
|
70
|
+
keep the circular hit/glow. The coin art fills the button and is the pulsed node on hover. */
|
|
71
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus.ge-bb-coin { background:none; border:none; }
|
|
72
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus .ge-bb-coin-art { display:flex; width:100%; height:100%; }
|
|
73
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus .ge-bb-coin-art svg { width:100%; height:100%; display:block; }
|
|
75
74
|
#${SHELL_ROOT_ID} .ge-shell-buybonus:hover { box-shadow:0 0 11px 1px var(--shell-accent); }
|
|
75
|
+
/* the coin needs no accent halo on hover/active — it carries its own gold look */
|
|
76
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus.ge-bb-coin:hover { box-shadow:none; }
|
|
76
77
|
#${SHELL_ROOT_ID} .ge-shell-buybonus:hover span { animation:ge-bb-pulse .7s ease-in-out infinite; }
|
|
77
78
|
#${SHELL_ROOT_ID} .ge-shell-buybonus:active { transform:scale(.96); }
|
|
78
79
|
#${SHELL_ROOT_ID} .ge-shell-buybonus[disabled] { filter:grayscale(.5) brightness(.72); box-shadow:none; cursor:default; }
|
|
@@ -138,7 +139,7 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
138
139
|
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin { width:84px; height:84px; font-size:66px; border-width:4px;
|
|
139
140
|
background:var(--shell-btn); color:var(--shell-btn-ink); }
|
|
140
141
|
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin:hover { background:var(--shell-btn); color:var(--shell-accent); box-shadow:none; }
|
|
141
|
-
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-buybonus { width:
|
|
142
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-buybonus { width:58px; height:58px; font-size:9px; border-width:2px; }
|
|
142
143
|
/* landscape overflow — size the column to content, centre it; JS scales the whole stack to fit */
|
|
143
144
|
#${SHELL_ROOT_ID} .ge-shell-barhost.ge-fit { left:50%; right:auto; width:max-content; max-width:none; }
|
|
144
145
|
#${SHELL_ROOT_ID} .ge-shell-barhost.ge-fit .ge-shell-bottom { width:max-content; }
|
|
@@ -349,12 +350,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
349
350
|
#${SHELL_ROOT_ID} .ge-bonus-vol { display:flex; justify-content:center; align-items:center; gap:0;
|
|
350
351
|
font-size:2.2em; line-height:1; margin-bottom:.55em; }
|
|
351
352
|
#${SHELL_ROOT_ID} .ge-bonus-vol-on, #${SHELL_ROOT_ID} .ge-bonus-vol-off { display:inline-flex; gap:0; }
|
|
352
|
-
/* Volatility
|
|
353
|
-
|
|
354
|
-
#${SHELL_ROOT_ID} .ge-bonus-vol svg {
|
|
355
|
-
#${SHELL_ROOT_ID} .ge-bonus-vol svg path {
|
|
356
|
-
#${SHELL_ROOT_ID} .ge-bonus-vol-on svg path { stroke:var(--card-acc); fill:var(--card-acc); }
|
|
357
|
-
#${SHELL_ROOT_ID} .ge-bonus-vol-off svg path { stroke:#000; fill:#000; }
|
|
353
|
+
/* Volatility = the turbo bolt (turbo1) repeated: active bolts filled accent, inactive filled black
|
|
354
|
+
and dimmed. Solid fill, no outline. */
|
|
355
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol-on svg path { fill:var(--card-acc); }
|
|
356
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol-off svg path { fill:#000; }
|
|
358
357
|
#${SHELL_ROOT_ID} .ge-bonus-vol-off { opacity:.5; }
|
|
359
358
|
#${SHELL_ROOT_ID} .ge-bonus-price { font-size:1.6em; font-weight:800; color:#fff; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
360
359
|
#${SHELL_ROOT_ID} .ge-bonus-cta { width:100%; border:none; padding:1.15em; font-weight:800; font-size:1.05em;
|
|
@@ -488,7 +487,7 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
488
487
|
|
|
489
488
|
/* BUY BONUS — floats outside-left of the bar; keeps its accent disc + hover ring (the one exception).
|
|
490
489
|
relative/z-index so it stacks predictably under the full-screen overlays (see overlay-stacking test). */
|
|
491
|
-
#${SHELL_ROOT_ID} .ge-shell-bottom > .ge-shell-buybonus { flex:0 0 auto; width:
|
|
490
|
+
#${SHELL_ROOT_ID} .ge-shell-bottom > .ge-shell-buybonus { flex:0 0 auto; width:71px; height:71px;
|
|
492
491
|
font-size:11px; border-width:3px; position:relative; z-index:3; }
|
|
493
492
|
|
|
494
493
|
/* WIN — a plain readout (same as balance/bet), grouped on the left with the other info */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Container, Graphics,
|
|
1
|
+
import { Container, Graphics, Text } from 'pixi.js';
|
|
2
2
|
import type { PixiComponentContext } from '../context';
|
|
3
|
-
import { effectiveAccent
|
|
3
|
+
import { effectiveAccent } from '@/core/colors';
|
|
4
|
+
import { BUY_BONUS_ART, BUY_BONUS_SOCIAL_ART, BUY_BONUS_DISABLED_ART } from '../../buy-bonus-art';
|
|
4
5
|
import { FlexBox } from '../primitives/flex';
|
|
5
6
|
import { roundedPath } from '../primitives/flex';
|
|
6
7
|
import {
|
|
@@ -10,10 +11,8 @@ import {
|
|
|
10
11
|
BuyBonusBadge,
|
|
11
12
|
FsHero,
|
|
12
13
|
divider,
|
|
13
|
-
attachHover,
|
|
14
|
-
attachPress,
|
|
15
14
|
} from '../primitives/widgets';
|
|
16
|
-
import {
|
|
15
|
+
import type { IconName } from '../icons';
|
|
17
16
|
|
|
18
17
|
// ── design constants (mirror the DOM `.ge-bar-panel` / mobile rules) ──────────
|
|
19
18
|
const BAR_H = 68; // continuous dark panel height
|
|
@@ -30,7 +29,7 @@ const BAR_MIN_SCALE = 0.5;
|
|
|
30
29
|
const WIDE_PAD_BOTTOM = 8;
|
|
31
30
|
export const WIDE_BAR_H = WIDE_PAD_BOTTOM + SPIN; // ≈92
|
|
32
31
|
|
|
33
|
-
const BUY_W =
|
|
32
|
+
const BUY_W = 71; // buy-bonus coin on desktop (+15% over the old 62 disc)
|
|
34
33
|
const DISC = 38; // white-disc icon button box (auto/turbo)
|
|
35
34
|
const FS_BOX = 90; // fixed bet-value box width
|
|
36
35
|
|
|
@@ -40,87 +39,36 @@ const M_CTRL_H = 62,
|
|
|
40
39
|
M_GAP = 10,
|
|
41
40
|
M_PAD_BOTTOM = 8,
|
|
42
41
|
M_SIDE = 10;
|
|
43
|
-
const M_BUY =
|
|
42
|
+
const M_BUY = 58; // mobile buy-bonus coin (+15%)
|
|
44
43
|
export const MOBILE_BAR_H = M_PAD_BOTTOM + SPIN + M_GAP + M_INFO_H; // spin pops above the controls
|
|
45
44
|
|
|
46
|
-
/** Three-state turbo
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
private box: number;
|
|
54
|
-
private view: IconView;
|
|
55
|
-
private discG: Graphics;
|
|
56
|
-
private discBorder: number;
|
|
57
|
-
private level: number;
|
|
58
|
-
private accent: string;
|
|
59
|
-
|
|
60
|
-
constructor(opts: {
|
|
61
|
-
size: number;
|
|
62
|
-
glyph: number;
|
|
63
|
-
discFill: string;
|
|
64
|
-
discBorder?: number;
|
|
65
|
-
accent: string;
|
|
66
|
-
level: number;
|
|
67
|
-
onTap: () => void;
|
|
68
|
-
}) {
|
|
69
|
-
super();
|
|
70
|
-
this.box = opts.size;
|
|
71
|
-
this.accent = opts.accent;
|
|
72
|
-
this.level = opts.level;
|
|
73
|
-
this.discBorder = opts.discBorder ?? 2;
|
|
74
|
-
this.discG = new Graphics();
|
|
75
|
-
this.addChild(this.discG);
|
|
76
|
-
const { fill, ring } = this._colors(false);
|
|
77
|
-
this.view = makeRingedIcon('turbo1', opts.glyph, fill, ring);
|
|
78
|
-
this.view.position.set((this.box - opts.glyph) / 2, (this.box - opts.glyph) / 2);
|
|
79
|
-
this.addChild(this.view);
|
|
80
|
-
// off is no longer dimmed — it reads as off via the black ring (DOM dropped the opacity:.5 rule)
|
|
81
|
-
this.eventMode = 'static';
|
|
82
|
-
this.cursor = 'pointer';
|
|
83
|
-
this.hitArea = new Rectangle(0, 0, this.box, this.box);
|
|
84
|
-
this._paint(false);
|
|
85
|
-
attachHover(
|
|
86
|
-
this,
|
|
87
|
-
() => this._paint(true),
|
|
88
|
-
() => this._paint(false),
|
|
89
|
-
);
|
|
90
|
-
attachPress(this, 0.92, () => opts.onTap());
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private _colors(hovering: boolean): { fill: string; ring: string } {
|
|
94
|
-
if (hovering) return { fill: this.accent, ring: this.accent };
|
|
95
|
-
if (this.level === 0) return { fill: '#ffffff', ring: '#000000' };
|
|
96
|
-
if (this.level === 1) return { fill: '#ffffff', ring: this.accent };
|
|
97
|
-
return { fill: this.accent, ring: this.accent };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private _paint(hovering: boolean): void {
|
|
101
|
-
const { fill, ring } = this._colors(hovering);
|
|
102
|
-
this.view.setColors(fill, ring);
|
|
103
|
-
const r = this.box / 2 - this.discBorder / 2;
|
|
104
|
-
this.discG.clear();
|
|
105
|
-
this.discG.circle(this.box / 2, this.box / 2, r).fill('#ffffff');
|
|
106
|
-
// border lights accent on hover OR when engaged (level>0) — mirrors DOM .ge-iconbtn.ge-active
|
|
107
|
-
const ringActive = hovering || this.level > 0;
|
|
108
|
-
this.discG.stroke({ color: ringActive ? this.accent : '#000000', width: this.discBorder });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Update turbo level and repaint (called when the shell re-renders the bar). */
|
|
112
|
-
setLevel(level: number, accent: string): void {
|
|
113
|
-
this.level = level;
|
|
114
|
-
this.accent = accent;
|
|
115
|
-
this._paint(false);
|
|
116
|
-
}
|
|
45
|
+
/** Three-state turbo glyph: the level is shown by SWAPPING the glyph (off → single bolt `turboOff`,
|
|
46
|
+
* L1 → single bolt `turbo1`, L≥2 → bolt-with-speed-lines `turbo2`). off = svg turbo-off grey (#ccc),
|
|
47
|
+
* engaged/hover = accent. On desktop it sits on the white disc (like auto); mobile is disc-less. */
|
|
48
|
+
function turboGlyph(level: number): IconName {
|
|
49
|
+
if (level <= 0) return 'turboOff';
|
|
50
|
+
return level >= 2 ? 'turbo2' : 'turbo1';
|
|
51
|
+
}
|
|
117
52
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
53
|
+
function makeTurboButton(
|
|
54
|
+
host: PixiComponentContext,
|
|
55
|
+
level: number,
|
|
56
|
+
onTap: () => void,
|
|
57
|
+
size = DISC,
|
|
58
|
+
glyph = 19,
|
|
59
|
+
disc = false,
|
|
60
|
+
): IconButton {
|
|
61
|
+
const { tokens } = host;
|
|
62
|
+
return new IconButton(turboGlyph(level), {
|
|
63
|
+
size,
|
|
64
|
+
glyph,
|
|
65
|
+
...(disc ? { disc: tokens.btn, discBorder: 2 } : {}),
|
|
66
|
+
color: '#cccccc', // svg turbo-off grey (off); engaged/hover switch to accent
|
|
67
|
+
hover: tokens.accent,
|
|
68
|
+
activeColor: tokens.accent,
|
|
69
|
+
active: level > 0,
|
|
70
|
+
onTap,
|
|
71
|
+
});
|
|
124
72
|
}
|
|
125
73
|
|
|
126
74
|
/** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
|
|
@@ -169,7 +117,7 @@ export class BottomBar extends Container {
|
|
|
169
117
|
private betDown?: IconButton;
|
|
170
118
|
private spin?: SpinDisc;
|
|
171
119
|
private autoBtn?: IconButton;
|
|
172
|
-
private turboBtn?:
|
|
120
|
+
private turboBtn?: IconButton;
|
|
173
121
|
|
|
174
122
|
constructor(host: PixiComponentContext) {
|
|
175
123
|
super();
|
|
@@ -310,15 +258,7 @@ export class BottomBar extends Container {
|
|
|
310
258
|
);
|
|
311
259
|
}
|
|
312
260
|
if (config.features.turbo > 0) {
|
|
313
|
-
this.turboBtn =
|
|
314
|
-
size: DISC,
|
|
315
|
-
glyph: 19,
|
|
316
|
-
discFill: tokens.btn,
|
|
317
|
-
discBorder: 2,
|
|
318
|
-
accent: tokens.accent,
|
|
319
|
-
level: state.turbo,
|
|
320
|
-
onTap: () => this.onTurbo(),
|
|
321
|
-
});
|
|
261
|
+
this.turboBtn = makeTurboButton(this.host, state.turbo, () => this.onTurbo(), DISC, 19, true);
|
|
322
262
|
spinWrap.add(this.turboBtn);
|
|
323
263
|
}
|
|
324
264
|
|
|
@@ -335,14 +275,13 @@ export class BottomBar extends Container {
|
|
|
335
275
|
if (config.features.buyBonus === false && !config.onBonusBuy) return null;
|
|
336
276
|
const feature = state.activeFeature;
|
|
337
277
|
if (feature) {
|
|
338
|
-
|
|
278
|
+
// a feature is active → the coin becomes the "deactivate" variant
|
|
339
279
|
return new BuyBonusBadge({
|
|
340
280
|
size,
|
|
341
|
-
fontSize,
|
|
342
281
|
border,
|
|
343
|
-
bg:
|
|
344
|
-
|
|
345
|
-
label:
|
|
282
|
+
bg: effectiveAccent(feature),
|
|
283
|
+
coinArt: BUY_BONUS_DISABLED_ART,
|
|
284
|
+
label: '',
|
|
346
285
|
tokens,
|
|
347
286
|
ticker: this.host.ticker,
|
|
348
287
|
onTap: () => this.host.actions.deactivateFeature(),
|
|
@@ -352,9 +291,7 @@ export class BottomBar extends Container {
|
|
|
352
291
|
size,
|
|
353
292
|
border,
|
|
354
293
|
bg: tokens.accent,
|
|
355
|
-
|
|
356
|
-
iconSize: size * 0.55,
|
|
357
|
-
iconColor: tokens.btnInk,
|
|
294
|
+
coinArt: config.isSocial ? BUY_BONUS_SOCIAL_ART : BUY_BONUS_ART,
|
|
358
295
|
label: '',
|
|
359
296
|
tokens,
|
|
360
297
|
ticker: this.host.ticker,
|
|
@@ -417,15 +354,7 @@ export class BottomBar extends Container {
|
|
|
417
354
|
if (state.autoplay.active) this.autoBtn.setGlow(true);
|
|
418
355
|
}
|
|
419
356
|
if (config.features.turbo > 0) {
|
|
420
|
-
this.turboBtn =
|
|
421
|
-
size: 40,
|
|
422
|
-
glyph: 22,
|
|
423
|
-
discFill: tokens.btn,
|
|
424
|
-
discBorder: 2,
|
|
425
|
-
accent: tokens.accent,
|
|
426
|
-
level: state.turbo,
|
|
427
|
-
onTap: () => this.onTurbo(),
|
|
428
|
-
});
|
|
357
|
+
this.turboBtn = makeTurboButton(this.host, state.turbo, () => this.onTurbo(), 40, 22);
|
|
429
358
|
}
|
|
430
359
|
const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
|
|
431
360
|
|
|
@@ -654,7 +583,11 @@ export class BottomBar extends Container {
|
|
|
654
583
|
);
|
|
655
584
|
|
|
656
585
|
this.inner.scale.set(s);
|
|
657
|
-
|
|
586
|
+
// Scale the bottom gap WITH the bar (…* s), not a flat 8px. On a small/short popout the bar is
|
|
587
|
+
// fit-scaled down but a constant 8px gap would look like too much air — so the bar floated off the
|
|
588
|
+
// bottom edge. Scaling matches the HTML shell, whose whole barhost scales from transform-origin
|
|
589
|
+
// bottom center (gap → 8·s). Desktop (s≈1) is unchanged.
|
|
590
|
+
this.inner.position.set((W - barW * s) / 2, H - (WIDE_PAD_BOTTOM + SPIN) * s);
|
|
658
591
|
this.measureFootprint();
|
|
659
592
|
}
|
|
660
593
|
|
|
@@ -4,7 +4,7 @@ import type { BonusOption } from '@/core/types';
|
|
|
4
4
|
import { betDir } from '@/core/keyboard';
|
|
5
5
|
import { effectiveAccent, contrastText } from '@/core/colors';
|
|
6
6
|
import { makeText } from '../text';
|
|
7
|
-
import { makeIcon
|
|
7
|
+
import { makeIcon } from '../pixi-icon';
|
|
8
8
|
import { navButton } from '../primitives/overlay';
|
|
9
9
|
import { clamp } from '../primitives/overlay';
|
|
10
10
|
import { attachHover } from '../primitives/widgets';
|
|
@@ -700,11 +700,11 @@ function volatilityRow(_host: PixiComponentContext, level: number, accent: strin
|
|
|
700
700
|
const n = Math.max(0, Math.min(5, level));
|
|
701
701
|
let x = 0;
|
|
702
702
|
for (let i = 0; i < 5; i++) {
|
|
703
|
-
// active = solid accent bolt; inactive = solid black bolt, dimmed
|
|
704
|
-
//
|
|
703
|
+
// active = solid accent bolt; inactive = solid black bolt, dimmed
|
|
704
|
+
// (DOM: .ge-bonus-vol svg path fill = accent (on) / #000 (off))
|
|
705
705
|
const active = i < n;
|
|
706
706
|
const col = active ? accent : '#000000';
|
|
707
|
-
const bolt =
|
|
707
|
+
const bolt = makeIcon('turbo1', size, col);
|
|
708
708
|
bolt.alpha = active ? 1 : 0.5;
|
|
709
709
|
bolt.position.set(x, 0);
|
|
710
710
|
c.addChild(bolt);
|
|
@@ -18,6 +18,7 @@ import { makeIcon } from '../pixi-icon';
|
|
|
18
18
|
import { FlexBox } from '../primitives/flex';
|
|
19
19
|
import { section, paragraph, Spacer } from '../primitives/controls';
|
|
20
20
|
import { BuyBonusBadge } from '../primitives/widgets';
|
|
21
|
+
import { BUY_BONUS_ART, BUY_BONUS_SOCIAL_ART } from '../../buy-bonus-art';
|
|
21
22
|
import { PACKAGE_VERSION } from '@/core/version';
|
|
22
23
|
|
|
23
24
|
/** Game info overlay — modes, controls, paytable, win illustrations, custom sections. */
|
|
@@ -190,11 +191,10 @@ function sectionControls(host: PixiComponentContext, title: string, width: numbe
|
|
|
190
191
|
const box = new Container();
|
|
191
192
|
const badge = new BuyBonusBadge({
|
|
192
193
|
size: 46,
|
|
193
|
-
fontSize: 8,
|
|
194
194
|
border: 2,
|
|
195
195
|
bg: host.tokens.accent,
|
|
196
|
-
|
|
197
|
-
label:
|
|
196
|
+
coinArt: host.config.isSocial ? BUY_BONUS_SOCIAL_ART : BUY_BONUS_ART,
|
|
197
|
+
label: '',
|
|
198
198
|
tokens: host.tokens,
|
|
199
199
|
ticker: host.ticker,
|
|
200
200
|
onTap: () => {},
|
|
@@ -523,7 +523,7 @@ function sectionCustom(host: PixiComponentContext, s: Extract<GameInfoSection, {
|
|
|
523
523
|
const sec = section(host, s.title != null ? host.t(s.title) : undefined);
|
|
524
524
|
const inner = width - SECTION_PAD;
|
|
525
525
|
if (s.node) {
|
|
526
|
-
sec.add(s.node as Container); // game-supplied Pixi content owns its own layout
|
|
526
|
+
sec.add(persistentSlot(s.node as Container)); // game-supplied Pixi content owns its own layout
|
|
527
527
|
} else if (s.html) {
|
|
528
528
|
const text = s.html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
529
529
|
if (text) sec.add(paragraph(host, text, inner, { size: 15 }));
|
|
@@ -579,6 +579,23 @@ function spacerBox(w: number, h: number): Graphics {
|
|
|
579
579
|
return g;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
/** Wrap a GAME-supplied custom `node` so the shell BORROWS it instead of owning it. The overlay is
|
|
583
|
+
* torn down with `destroy({ children: true })` on every close; that would recursively destroy the
|
|
584
|
+
* game's node, so the section renders blank on reopen. This slot's `destroy` DETACHES the node
|
|
585
|
+
* (leaving it intact) and destroys only the empty wrapper — the SAME node instance is re-mounted,
|
|
586
|
+
* with all its drawing, the next time Game info opens. Mirrors the HTML shell, where the game's DOM
|
|
587
|
+
* node simply persists in config across open/close. The game owns the node's lifetime, not us. */
|
|
588
|
+
function persistentSlot(node: Container): Container {
|
|
589
|
+
const slot = new Container();
|
|
590
|
+
slot.addChild(node);
|
|
591
|
+
const destroySelf = Container.prototype.destroy.bind(slot);
|
|
592
|
+
slot.destroy = () => {
|
|
593
|
+
if (node.parent === slot) slot.removeChild(node); // keep the game node alive for the next open
|
|
594
|
+
destroySelf({ children: false });
|
|
595
|
+
};
|
|
596
|
+
return slot;
|
|
597
|
+
}
|
|
598
|
+
|
|
582
599
|
/** A fixed box that loads an image into it (object-fit: contain) once the texture resolves. */
|
|
583
600
|
function imageBox(url: string, w: number, h: number): Container {
|
|
584
601
|
const c = new Container();
|
package/src/ui/pixi/icons.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
// AUTO-GENERATED from
|
|
1
|
+
// AUTO-GENERATED from ../../icons.svg by scripts/gen-icons-from-svg.mjs — do not edit by hand.
|
|
2
2
|
// Sharp monochrome icon set: each glyph is a 24×24 viewBox fragment using currentColor
|
|
3
|
-
// (hollow shapes use fill-rule="evenodd"
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// (hollow shapes use fill-rule="evenodd"). Coordinates are baked into the 24×24 space (no
|
|
4
|
+
// <g transform>), so the same fragment renders identically in the DOM <svg> and in Pixi's
|
|
5
|
+
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
6
|
+
// `node scripts/gen-icons-from-svg.mjs`.
|
|
7
7
|
const SVGS: Record<string, string> = {
|
|
8
|
-
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
9
|
-
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
10
|
-
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
11
|
-
stop: `<g fill="currentColor" fill-rule="evenodd"><
|
|
12
|
-
menu: `<g fill="currentColor" fill-rule="evenodd"><
|
|
13
|
-
minus: `<g fill="currentColor" fill-rule="evenodd"><
|
|
14
|
-
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
8
|
+
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="M13.57 8.41C13.3 8.31 13.51 7.09 13.59 6.65C13.61 6.56 13.55 6.48 13.46 6.48C12.32 6.45 5.74 6.61 4.16 14.85C4.15 14.9 4 15.16 3.8 14.91C3.27 14.23 2.43 12.08 3.46 9.16C6.01 1.92 13 3.07 13.79 3.23C13.83 3.23 13.86 3.2 13.86 3.17C13.85 2.86 13.82 1.68 14.17 1.64C14.51 1.6 19.04 5.61 18.96 5.95C18.86 6.36 13.83 8.51 13.57 8.41Z"/><path d="M10.43 15.59C10.7 15.69 10.49 16.91 10.41 17.35C10.39 17.44 10.45 17.52 10.54 17.52C11.68 17.55 18.26 17.39 19.84 9.15C19.85 9.1 20 8.84 20.2 9.09C20.73 9.77 21.57 11.92 20.54 14.85C17.99 22.08 11 20.93 10.21 20.77C10.17 20.77 10.13 20.8 10.14 20.83C10.15 21.14 10.18 22.32 9.83 22.36C9.49 22.4 4.96 18.39 5.04 18.05C5.14 17.64 10.17 15.49 10.43 15.59Z"/></g>`,
|
|
9
|
+
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
10
|
+
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.97 12.41C20.22 11.94 20.95 11.88 20.97 12.47C21.14 16.4 18.71 19.92 14.44 20.97C8.59 22.4 5.29 18.55 5.28 18.52C5.2 18.33 4.41 19.77 4.19 19.59C3.81 19.28 3.78 14.9 3.9 14.78C4.01 14.67 8.39 14.95 8.48 15.31C8.58 15.64 7.2 16.1 7.4 16.38C9.14 18.67 15.82 20.29 19.97 12.41Z"/><path d="M9.3 15.36L9.3 8.67C9.3 8.32 9.97 7.74 10.58 8.14C15.83 11.56 16 11.46 16.01 12.04C16.02 12.61 15.12 12.98 10.63 15.91C10.32 16.11 9.29 15.72 9.29 15.36Z"/><path d="M4.03 11.59C3.78 12.06 3.05 12.12 3.03 11.53C2.86 7.6 5.29 4.08 9.56 3.03C15.41 1.6 18.71 5.45 18.72 5.48C18.8 5.67 19.59 4.23 19.81 4.41C20.19 4.72 20.22 9.1 20.1 9.22C19.99 9.33 15.61 9.05 15.52 8.69C15.42 8.36 16.8 7.9 16.6 7.62C14.86 5.33 8.18 3.71 4.03 11.59Z"/></g>`,
|
|
11
|
+
stop: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="1.6" width="20.8" height="20.8" rx="2.9"/></g>`,
|
|
12
|
+
menu: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.61" y="3.81" width="20.79" height="4.47" rx="2.23"/><rect x="1.6" y="9.77" width="20.79" height="4.46" rx="2.23"/><rect x="1.6" y="15.72" width="20.79" height="4.47" rx="2.23"/></g>`,
|
|
13
|
+
minus: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="9.86" width="20.8" height="4.28" rx="2.11"/></g>`,
|
|
14
|
+
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.72 14.37L13.84 14.37L13.84 20.33C13.84 21.48 12.89 22.4 11.74 22.36C10.61 22.36 9.7 21.45 9.7 20.33L9.7 14.37L3.67 14.37C2.52 14.37 1.6 13.42 1.63 12.27C1.63 11.15 2.55 10.23 3.67 10.23L9.7 10.23L9.7 4.28C9.71 1.61 13.83 1.6 13.84 4.28L13.84 10.23L19.72 10.23C22.39 10.24 22.4 14.36 19.72 14.37Z"/></g>`,
|
|
15
15
|
gift: `<rect x="4" y="9" width="16" height="11" rx="2" fill="currentColor"/><path d="M9 9c-1.35 -0.31 -2.18 -1.65 -1.87 -3s1.65 -2.18 3 -1.87c0.93 0.22 1.65 0.94 1.87 1.87c0.31 -1.35 1.65 -2.18 3 -1.87c1.35 0.31 2.18 1.65 1.87 3c-0.22 0.93 -0.94 1.65 -1.87 1.87h-6Z" fill="currentColor"/><rect x="11" y="9" width="2" height="11" fill="rgba(0,0,0,.35)"/>`,
|
|
16
|
-
info: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
17
|
-
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
18
|
-
soundOff: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
19
|
-
close: `<g fill="currentColor" fill-rule="evenodd"><path d="
|
|
20
|
-
back: `<
|
|
21
|
-
chevronRight: `<
|
|
16
|
+
info: `<g fill="currentColor" fill-rule="evenodd"><circle cx="11.98" cy="4.27" r="2.67"/><path d="M16.05 21.09L16.05 21.54C16.05 22.02 15.67 22.4 15.19 22.4L8.89 22.4C8.42 22.4 8.04 22.02 8.04 21.54L8.04 20.98C8.04 20.5 8.42 20.12 8.89 20.12L8.89 20.12C9.13 20.12 9.32 19.93 9.32 19.7L9.32 11.59C9.32 11.29 9.08 11.05 8.8 11.05C8.33 11.05 7.95 10.67 7.95 10.19L7.95 9.18C7.95 8.7 8.33 8.32 8.8 8.32L9.32 8.32C9.32 8.32 13.8 8.32 13.8 8.32C14.27 8.32 14.65 8.71 14.65 9.18L14.65 19.7C14.65 19.93 14.84 20.12 15.07 20.12L15.07 20.12C15.61 20.12 16.05 20.56 16.05 21.09Z"/></g>`,
|
|
17
|
+
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 15.04L1.6 9.18C1.6 8.75 1.95 8.4 2.38 8.4L5.03 8.4C5.21 8.4 5.39 8.36 5.56 8.28L11.6 5.26C12.12 5 12.73 5.38 12.73 5.96L12.73 18.27C12.73 18.85 12.12 19.22 11.6 18.96L5.56 15.94C5.39 15.86 5.22 15.82 5.03 15.82L2.38 15.82C1.95 15.82 1.6 15.47 1.6 15.04Z"/><path d="M16.01 12.11C16.04 12.36 15.67 14.7 14.7 15.23C13.92 15.42 13.54 14.77 13.86 14.1C14.89 12.56 14.88 11.66 13.86 10.13C13.54 9.46 13.92 8.8 14.7 8.99C15.67 9.53 16.06 11.81 16.01 12.11Z"/><path d="M19.15 12.12C19.2 12.51 18.6 16.4 17.3 17.05C16.32 17.54 15.99 16.87 16.42 15.81C17.8 13.4 17.59 9.93 16.42 8.41C15.73 7.5 16.34 6.58 17.26 7.18C18.74 8.15 19.21 11.64 19.15 12.12Z"/><path d="M22.4 12.16C22.16 15.18 21 17.95 19.71 18.82C18.73 19.48 18.22 18.42 18.93 17.32C20.96 14.17 21.92 9.95 18.9 6.29C18.13 5.35 19.35 4.52 20.11 5.48C21.43 7.14 22.33 9.14 22.4 12.16Z"/></g>`,
|
|
18
|
+
soundOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 14.77L1.6 9.23C1.6 8.82 1.93 8.49 2.34 8.49L4.85 8.49C5.02 8.49 5.19 8.45 5.34 8.38L11.06 5.52C11.55 5.27 12.12 5.63 12.12 6.18L12.12 17.82C12.12 18.37 11.55 18.73 11.06 18.48L5.34 15.62C5.19 15.55 5.02 15.51 4.85 15.51L2.34 15.51C1.93 15.51 1.6 15.18 1.6 14.77Z"/><path d="M21.95 16.53C21.5 16.98 20.78 16.98 20.33 16.53L17.79 13.99L15.25 16.54C14.8 16.99 14.07 16.99 13.62 16.54C13.17 16.09 13.17 15.36 13.62 14.92L16.17 12.37L13.63 9.83C13.18 9.38 13.18 8.65 13.63 8.21C13.85 7.98 14.14 7.87 14.44 7.87C14.73 7.87 15.02 7.98 15.25 8.21L17.79 10.75L20.33 8.21C20.77 7.76 21.5 7.76 21.95 8.21C22.17 8.44 22.28 8.73 22.28 9.02C22.28 9.32 22.17 9.61 21.95 9.83L19.41 12.37L21.95 14.91C22.4 15.36 22.4 16.08 21.95 16.53Z"/></g>`,
|
|
19
|
+
close: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.39 21.37C20.38 22.38 18.74 22.38 17.73 21.37L12.01 15.64L6.27 21.38C5.26 22.39 3.62 22.39 2.61 21.38C1.6 20.38 1.6 18.74 2.61 17.73L8.35 11.99L2.62 6.25C1.61 5.24 1.61 3.61 2.62 2.6C3.12 2.09 3.79 1.84 4.45 1.84C5.11 1.84 5.77 2.09 6.28 2.6L12.01 8.33L17.72 2.62C18.73 1.61 20.37 1.61 21.38 2.62C21.89 3.12 22.14 3.78 22.14 4.44C22.14 5.11 21.89 5.77 21.38 6.27L15.67 11.99L21.39 17.71C22.4 18.72 22.4 20.36 21.39 21.37Z"/></g>`,
|
|
20
|
+
back: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.49 2.32L17.49 2.32C18.2 3.03 18.2 4.19 17.49 4.9L11.68 10.71C10.97 11.42 10.97 12.58 11.68 13.29L17.49 19.1C18.21 19.81 18.21 20.97 17.49 21.69L17.49 21.69C16.78 22.4 15.62 22.4 14.91 21.69L7.8 14.58L7.8 14.58C7.8 14.58 6.51 13.29 6.51 13.29C5.79 12.58 5.79 11.42 6.51 10.71L14.9 2.31C15.62 1.6 16.77 1.6 17.49 2.31Z"/></g>`,
|
|
21
|
+
chevronRight: `<g fill="currentColor" fill-rule="evenodd"><path d="M6.51 21.68L6.51 21.68C5.8 20.97 5.8 19.81 6.51 19.1L12.32 13.29C13.03 12.58 13.03 11.42 12.32 10.71L6.51 4.9C5.79 4.19 5.79 3.03 6.51 2.31L6.51 2.31C7.22 1.6 8.38 1.6 9.09 2.31L16.2 9.42L16.2 9.42C16.2 9.42 17.49 10.71 17.49 10.71C18.21 11.42 18.21 12.58 17.49 13.29L9.1 21.69C8.38 22.4 7.23 22.4 6.51 21.69Z"/></g>`,
|
|
22
22
|
ticket: `<path d="M1.72 9.48L19.64 4.67L20.28 7.07C19.19 7.8 18.82 9 19.11 10.09C19.41 11.19 20.33 12.04 21.64 12.13L22.28 14.52L4.36 19.32L3.72 16.93C4.81 16.2 5.18 15 4.89 13.91C4.59 12.81 3.67 11.96 2.36 11.87L1.72 9.48Z" fill="none" stroke="currentColor" stroke-width="1.84" stroke-linejoin="miter"/><path d="M12.57 7.17L9.75 13.26L11.92 12.4L11.84 16.74L14.85 10.06L12.38 11.1Z" fill="currentColor"/>`,
|
|
23
|
+
turbo2: `<g fill="currentColor" fill-rule="evenodd"><path d="M8.24 8.81L3.92 7.68C3.88 7.67 3.88 7.61 3.92 7.61L9.66 6.45C9.8 6.43 9.89 6.56 9.83 6.68L8.86 8.35C8.67 8.73 8.33 8.83 8.24 8.81Z"/><path d="M20.58 3.04L17.34 8.38C17.09 8.8 17.39 9.33 17.87 9.33L21.57 9.33C22.12 9.33 22.4 9.99 22.02 10.39L11.24 21.73C11.12 21.86 10.95 21.93 10.77 21.92L10.77 21.92C10.34 21.92 10.04 21.47 10.2 21.07L13.04 13.82C13.21 13.41 12.9 12.96 12.45 12.97L8.46 13.01C7.98 13.01 7.67 12.5 7.9 12.08L13.2 2.4C13.31 2.2 13.52 2.07 13.75 2.07L20.05 2.09C20.54 2.1 20.84 2.63 20.58 3.04Z"/><path d="M5.96 12.68L1.64 11.54C1.6 11.54 1.6 11.47 1.64 11.47L7.38 10.31C7.52 10.3 7.61 10.42 7.55 10.55L6.58 12.22C6.39 12.59 6.05 12.69 5.96 12.68Z"/><path d="M9.67 16.66L5.35 15.53C5.31 15.52 5.31 15.46 5.35 15.46L11.09 14.3C11.22 14.28 11.32 14.41 11.26 14.53L10.29 16.2C10.1 16.58 9.76 16.68 9.67 16.66Z"/></g>`,
|
|
24
|
+
turboOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
25
|
+
chevronUp: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.68 17.49L21.68 17.49C20.97 18.2 19.81 18.2 19.1 17.49L13.29 11.68C12.58 10.97 11.42 10.97 10.71 11.68L4.9 17.49C4.19 18.21 3.03 18.21 2.31 17.49L2.31 17.49C1.6 16.78 1.6 15.62 2.31 14.91L9.42 7.8L9.42 7.8C9.42 7.8 10.71 6.51 10.71 6.51C11.42 5.79 12.58 5.79 13.29 6.51L21.69 14.9C22.4 15.62 22.4 16.77 21.69 17.49Z"/></g>`,
|
|
26
|
+
chevronDown: `<g fill="currentColor" fill-rule="evenodd"><path d="M2.32 6.51L2.32 6.51C3.03 5.8 4.19 5.8 4.9 6.51L10.71 12.32C11.42 13.03 12.58 13.03 13.29 12.32L19.1 6.51C19.82 5.79 20.97 5.79 21.69 6.51L21.69 6.51C22.4 7.22 22.4 8.38 21.69 9.09L14.58 16.19L14.58 16.2C14.58 16.2 13.29 17.49 13.29 17.49C12.58 18.21 11.42 18.21 10.71 17.49L2.31 9.1C1.6 8.38 1.6 7.23 2.31 6.51Z"/></g>`,
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
export type IconName = keyof typeof SVGS;
|
|
@@ -35,11 +39,3 @@ export function icon(name: IconName): string {
|
|
|
35
39
|
export function iconSVG(name: IconName, color = '#ffffff'): string {
|
|
36
40
|
return `<svg viewBox="0 0 24 24">${SVGS[name].split('currentColor').join(color)}</svg>`;
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
/** A standalone SVG with the glyph rendered as a stroke-only outline (fill:none).
|
|
40
|
-
* Used to build the outer-ring effect in Pixi: draw this first (under), then iconSVG on top;
|
|
41
|
-
* only the outer half of the stroke is visible — the paint-order:stroke CSS equivalent. */
|
|
42
|
-
export function iconStrokeSVG(name: IconName, stroke: string, width = 4): string {
|
|
43
|
-
const frag = SVGS[name].split('fill="currentColor"').join(`fill="none" stroke="${stroke}" stroke-width="${width}" stroke-linejoin="round"`);
|
|
44
|
-
return `<svg viewBox="0 0 24 24">${frag}</svg>`;
|
|
45
|
-
}
|
package/src/ui/pixi/pixi-icon.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { Container, Graphics, GraphicsContext } from 'pixi.js';
|
|
2
|
-
import { iconSVG,
|
|
1
|
+
import { Container, Graphics, GraphicsContext, GraphicsContextSystem } from 'pixi.js';
|
|
2
|
+
import { iconSVG, type IconName } from './icons';
|
|
3
3
|
import type { Sizable } from './primitives/flex';
|
|
4
4
|
|
|
5
|
+
// Icons/coins are parsed from small-viewBox SVGs (24-unit glyphs, ~260-unit coin). Pixi's default
|
|
6
|
+
// bezierSmoothness (0.5) flattens curves with a ~1-unit path-space tolerance — coarse facets at
|
|
7
|
+
// that scale that magnify when the glyph is scaled to button size. Max it out (capped at 0.99) so
|
|
8
|
+
// every SVG-parsed curve stays smooth. Global default; set once, before any GraphicsContext builds.
|
|
9
|
+
GraphicsContextSystem.defaultOptions.bezierSmoothness = 0.99;
|
|
10
|
+
|
|
5
11
|
// The DOM shell renders each icon as an inline <svg> sized to 1em and recoloured via
|
|
6
12
|
// `currentColor`. The Pixi shell rebuilds the same 24×24 vector paths through Pixi's SVG
|
|
7
13
|
// parser (GraphicsContext.svg) and scales them to the requested pixel size — same shapes,
|
|
@@ -10,49 +16,33 @@ import type { Sizable } from './primitives/flex';
|
|
|
10
16
|
|
|
11
17
|
const VIEWBOX = 24;
|
|
12
18
|
|
|
13
|
-
// Cache parsed contexts by `name|fill
|
|
19
|
+
// Cache parsed contexts by `name|fill` — many icons repeat across the bar at the same colour.
|
|
14
20
|
const cache = new Map<string, GraphicsContext>();
|
|
15
21
|
|
|
16
|
-
function context(name: IconName, color: string
|
|
17
|
-
const key = `${name}|${color}
|
|
22
|
+
function context(name: IconName, color: string): GraphicsContext {
|
|
23
|
+
const key = `${name}|${color}`;
|
|
18
24
|
let ctx = cache.get(key);
|
|
19
25
|
if (!ctx) {
|
|
20
|
-
|
|
21
|
-
// Two-pass layering: stroke context first (under), then fill on top.
|
|
22
|
-
// Only the outer half of the stroke shows — the paint-order:stroke CSS equivalent.
|
|
23
|
-
ctx = new GraphicsContext()
|
|
24
|
-
.svg(iconStrokeSVG(name, ring, ringWidth))
|
|
25
|
-
.svg(iconSVG(name, color));
|
|
26
|
-
} else {
|
|
27
|
-
ctx = new GraphicsContext().svg(iconSVG(name, color));
|
|
28
|
-
}
|
|
26
|
+
ctx = new GraphicsContext().svg(iconSVG(name, color));
|
|
29
27
|
cache.set(key, ctx);
|
|
30
28
|
}
|
|
31
29
|
return ctx;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
/** A recolourable, scalable icon. Its content is centred on the local origin's box so the view's
|
|
35
|
-
* bounds are `size × size` and `rotation` spins around the glyph centre (like CSS 50% 50%).
|
|
36
|
-
*
|
|
37
|
-
* Optional `ring` and `ringWidth` enable the outer-ring treatment (fill-on-top-of-stroke): the
|
|
38
|
-
* stroke is drawn first (underneath), then the fill path is drawn on top — only the outer half
|
|
39
|
-
* of the stroke is visible, mirroring the DOM `paint-order:stroke` technique. */
|
|
33
|
+
* bounds are `size × size` and `rotation` spins around the glyph centre (like CSS 50% 50%). */
|
|
40
34
|
export class IconView extends Container implements Sizable {
|
|
41
35
|
private gfx: Graphics;
|
|
42
36
|
private _size: number;
|
|
43
37
|
private _color: string;
|
|
44
|
-
private _ring: string | undefined;
|
|
45
|
-
private _ringWidth: number;
|
|
46
38
|
readonly iconName: IconName;
|
|
47
39
|
|
|
48
|
-
constructor(name: IconName, size: number, color = '#ffffff'
|
|
40
|
+
constructor(name: IconName, size: number, color = '#ffffff') {
|
|
49
41
|
super();
|
|
50
42
|
this.iconName = name;
|
|
51
43
|
this._size = size;
|
|
52
44
|
this._color = color;
|
|
53
|
-
this.
|
|
54
|
-
this._ringWidth = ringWidth;
|
|
55
|
-
this.gfx = new Graphics(context(name, color, ring, ringWidth));
|
|
45
|
+
this.gfx = new Graphics(context(name, color));
|
|
56
46
|
this.addChild(this.gfx);
|
|
57
47
|
this.layout();
|
|
58
48
|
}
|
|
@@ -69,20 +59,11 @@ export class IconView extends Container implements Sizable {
|
|
|
69
59
|
}
|
|
70
60
|
|
|
71
61
|
setColor(color: string): void {
|
|
72
|
-
if (color === this._color
|
|
62
|
+
if (color === this._color) return;
|
|
73
63
|
this._color = color;
|
|
74
|
-
this._ring = undefined;
|
|
75
64
|
this.gfx.context = context(this.iconName, color);
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
/** Update fill colour and ring colour simultaneously (ring is preserved if provided). */
|
|
79
|
-
setColors(fill: string, ring?: string): void {
|
|
80
|
-
if (fill === this._color && ring === this._ring) return;
|
|
81
|
-
this._color = fill;
|
|
82
|
-
this._ring = ring;
|
|
83
|
-
this.gfx.context = context(this.iconName, fill, ring, this._ringWidth);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
67
|
setSize(size: number): void {
|
|
87
68
|
if (size === this._size) return;
|
|
88
69
|
this._size = size;
|
|
@@ -111,9 +92,3 @@ export class IconView extends Container implements Sizable {
|
|
|
111
92
|
export function makeIcon(name: IconName, size: number, color = '#ffffff'): IconView {
|
|
112
93
|
return new IconView(name, size, color);
|
|
113
94
|
}
|
|
114
|
-
|
|
115
|
-
/** Create a ringed icon — glyph filled with `fill`, outer ring coloured `ring` (width `ringWidth`).
|
|
116
|
-
* Uses the two-pass stroke-under-fill technique to show only the outer half of the stroke. */
|
|
117
|
-
export function makeRingedIcon(name: IconName, size: number, fill: string, ring: string, ringWidth = 4): IconView {
|
|
118
|
-
return new IconView(name, size, fill, ring, ringWidth);
|
|
119
|
-
}
|