@energy8platform/shell 0.2.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/dist/html.cjs.js +3894 -0
- package/dist/html.cjs.js.map +1 -0
- package/dist/html.d.ts +616 -0
- package/dist/html.esm.js +3879 -0
- package/dist/html.esm.js.map +1 -0
- package/dist/index.cjs.js +1593 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.esm.js +1582 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/pixi.cjs.js +5740 -0
- package/dist/pixi.cjs.js.map +1 -0
- package/dist/pixi.d.ts +682 -0
- package/dist/pixi.esm.js +5726 -0
- package/dist/pixi.esm.js.map +1 -0
- package/package.json +79 -0
- package/src/core/EventEmitter.ts +55 -0
- package/src/core/ShellController.ts +234 -0
- package/src/core/colors.ts +32 -0
- package/src/core/fonts-digits.ts +12 -0
- package/src/core/fonts.ts +13 -0
- package/src/core/format.ts +39 -0
- package/src/core/i18n.ts +96 -0
- package/src/core/index.ts +35 -0
- package/src/core/keyboard.ts +229 -0
- package/src/core/locales.ts +864 -0
- package/src/core/motion.ts +10 -0
- package/src/core/renderer.ts +121 -0
- package/src/core/state.ts +31 -0
- package/src/core/theme.ts +81 -0
- package/src/core/types.ts +269 -0
- package/src/core/version.ts +3 -0
- package/src/ui/html/HtmlRenderer.ts +292 -0
- package/src/ui/html/components/BottomBar.ts +240 -0
- package/src/ui/html/components/BuyBonus.ts +326 -0
- package/src/ui/html/components/GameInfo.ts +384 -0
- package/src/ui/html/components/Modal.ts +36 -0
- package/src/ui/html/components/ReplayModal.ts +58 -0
- package/src/ui/html/components/Settings.ts +59 -0
- package/src/ui/html/components/pickers.ts +146 -0
- package/src/ui/html/icons-preview.svg +224 -0
- package/src/ui/html/icons.ts +31 -0
- package/src/ui/html/index.ts +36 -0
- package/src/ui/html/motion-dom.ts +29 -0
- package/src/ui/html/primitives.ts +85 -0
- package/src/ui/html/shell.css.ts +528 -0
- package/src/ui/html/theme-css.ts +21 -0
- package/src/ui/pixi/PixiRenderer.ts +350 -0
- package/src/ui/pixi/components/BottomBar.ts +477 -0
- package/src/ui/pixi/components/BuyBonus.ts +763 -0
- package/src/ui/pixi/components/GameInfo.ts +559 -0
- package/src/ui/pixi/components/Modal.ts +40 -0
- package/src/ui/pixi/components/ReplayModal.ts +80 -0
- package/src/ui/pixi/components/Settings.ts +117 -0
- package/src/ui/pixi/components/pickers.ts +170 -0
- package/src/ui/pixi/context.ts +52 -0
- package/src/ui/pixi/icons.ts +45 -0
- package/src/ui/pixi/index.ts +56 -0
- package/src/ui/pixi/motion-pixi.ts +58 -0
- package/src/ui/pixi/pixi-icon.ts +119 -0
- package/src/ui/pixi/primitives/card.ts +227 -0
- package/src/ui/pixi/primitives/controls.ts +243 -0
- package/src/ui/pixi/primitives/flex.ts +335 -0
- package/src/ui/pixi/primitives/overlay.ts +181 -0
- package/src/ui/pixi/primitives/scroll.ts +117 -0
- package/src/ui/pixi/primitives/widgets.ts +680 -0
- package/src/ui/pixi/text.ts +131 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Container, Graphics, Rectangle, Text } from 'pixi.js';
|
|
2
|
+
import type { PixiComponentContext, ShellLayer } from '../context';
|
|
3
|
+
import { contrastText } from '@/core/colors';
|
|
4
|
+
import { makeIcon } from '../pixi-icon';
|
|
5
|
+
import { makeText } from '../text';
|
|
6
|
+
import { attachHover, attachPress } from './widgets';
|
|
7
|
+
import { FlexBox, roundedPath } from './flex';
|
|
8
|
+
|
|
9
|
+
/** Fraction of the frame a card modal may occupy (GameShell.MODAL_FIT). */
|
|
10
|
+
const MODAL_FIT = 0.86;
|
|
11
|
+
|
|
12
|
+
export interface CardAction {
|
|
13
|
+
label: string;
|
|
14
|
+
/** 'accent' (filled accent), 'ghost' (glass), or an explicit CSS colour. */
|
|
15
|
+
kind: 'accent' | 'ghost' | string;
|
|
16
|
+
onTap: () => void;
|
|
17
|
+
/** accent colour for the 'accent' kind (defaults to shell accent / card accent). */
|
|
18
|
+
accent?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CardOpts {
|
|
23
|
+
title: string;
|
|
24
|
+
accent?: string;
|
|
25
|
+
closable?: boolean;
|
|
26
|
+
/** unused for now — backdrop blur is approximated by the veil tint. */
|
|
27
|
+
blur?: number;
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
tag?: string;
|
|
30
|
+
/** Max card width in em (default 28). The bet picker uses 44em to fit 6 chips/row. */
|
|
31
|
+
maxEm?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A centred card on a frosted backdrop: accent title heading, vertical body, full-bleed footer
|
|
35
|
+
* buttons (corners clipped by the card silhouette), and an overlay ✕ pinned to the screen corner.
|
|
36
|
+
* Shared by the buy-bonus confirm, the bet/autoplay pickers, the generic + replay modals. */
|
|
37
|
+
export class CardModal extends Container implements ShellLayer {
|
|
38
|
+
readonly tag?: string;
|
|
39
|
+
readonly body: FlexBox;
|
|
40
|
+
private host: PixiComponentContext;
|
|
41
|
+
private opts: CardOpts;
|
|
42
|
+
private veil = new Graphics();
|
|
43
|
+
private cardRoot = new Container();
|
|
44
|
+
private bg = new Graphics();
|
|
45
|
+
private actionsRow = new Container();
|
|
46
|
+
private actionsH = 0;
|
|
47
|
+
private closeBtn?: Container;
|
|
48
|
+
private em: number;
|
|
49
|
+
private cardW: number;
|
|
50
|
+
private cardH = 0;
|
|
51
|
+
private accent: string;
|
|
52
|
+
|
|
53
|
+
constructor(host: PixiComponentContext, opts: CardOpts) {
|
|
54
|
+
super();
|
|
55
|
+
this.host = host;
|
|
56
|
+
this.opts = opts;
|
|
57
|
+
this.tag = opts.tag;
|
|
58
|
+
this.accent = opts.accent ?? host.tokens.accent;
|
|
59
|
+
this.em = clampEm(host.screenW, host.screenH);
|
|
60
|
+
this.cardW = Math.min((opts.maxEm ?? 28) * this.em, host.screenW * 0.86);
|
|
61
|
+
|
|
62
|
+
this.body = new FlexBox({
|
|
63
|
+
direction: 'column',
|
|
64
|
+
align: 'center',
|
|
65
|
+
gap: 1.05 * this.em,
|
|
66
|
+
padding: 1.2 * this.em,
|
|
67
|
+
});
|
|
68
|
+
const title = makeText(opts.title, {
|
|
69
|
+
size: 1.2 * this.em,
|
|
70
|
+
weight: '800',
|
|
71
|
+
color: this.accent,
|
|
72
|
+
letterSpacing: 1.2 * this.em * 0.04,
|
|
73
|
+
upper: true,
|
|
74
|
+
align: 'center',
|
|
75
|
+
wrapWidth: this.cardW - 2.4 * this.em,
|
|
76
|
+
});
|
|
77
|
+
this.body.add(title);
|
|
78
|
+
|
|
79
|
+
this.veil.eventMode = 'static';
|
|
80
|
+
// No mask on cardRoot: a masked container blocks pointer events to its children (chips/buttons
|
|
81
|
+
// wouldn't hover). The only thing that needs clipping is the full-bleed action buttons' bottom
|
|
82
|
+
// corners, which we round directly (setActions) — the body content never reaches the corners.
|
|
83
|
+
this.cardRoot.addChild(this.bg, this.body, this.actionsRow);
|
|
84
|
+
this.addChild(this.veil, this.cardRoot);
|
|
85
|
+
|
|
86
|
+
if (opts.closable !== false && opts.onClose) this.addClose(opts.onClose);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get emSize(): number {
|
|
90
|
+
return this.em;
|
|
91
|
+
}
|
|
92
|
+
get cardWidth(): number {
|
|
93
|
+
return this.cardW;
|
|
94
|
+
}
|
|
95
|
+
get cardAccent(): string {
|
|
96
|
+
return this.accent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Full-bleed footer buttons, flush to the card's bottom edge. */
|
|
100
|
+
setActions(actions: CardAction[]): void {
|
|
101
|
+
this.actionsRow.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
102
|
+
if (!actions.length) return;
|
|
103
|
+
const each = this.cardW / actions.length;
|
|
104
|
+
const h = 3.1 * this.em;
|
|
105
|
+
const r = 1.3 * this.em; // card corner radius — only the outermost buttons round at the bottom
|
|
106
|
+
actions.forEach((a, i) => {
|
|
107
|
+
const btn = this.makeButton(a, each, h, i === 0 ? r : 0, i === actions.length - 1 ? r : 0);
|
|
108
|
+
btn.position.set(i * each, 0);
|
|
109
|
+
this.actionsRow.addChild(btn);
|
|
110
|
+
});
|
|
111
|
+
this.actionsH = h;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private makeButton(a: CardAction, w: number, h: number, blRadius: number, brRadius: number): Container {
|
|
115
|
+
const root = new Container();
|
|
116
|
+
const fill =
|
|
117
|
+
a.kind === 'accent' ? (a.accent ?? this.accent) : a.kind === 'ghost' ? this.host.tokens.plaqueGlassHover : a.kind;
|
|
118
|
+
const ink = a.kind === 'ghost' ? '#ffffff' : a.kind === 'accent' ? '#ffffff' : contrastText(fill);
|
|
119
|
+
const bg = new Graphics();
|
|
120
|
+
const draw = (hover: boolean): void => {
|
|
121
|
+
bg.clear();
|
|
122
|
+
roundedPath(bg, 0, 0, w, h, [0, 0, brRadius, blRadius]); // square top (flush to body), rounded bottom
|
|
123
|
+
bg.fill(a.disabled ? '#8d939e' : fill);
|
|
124
|
+
if (hover && !a.disabled) {
|
|
125
|
+
roundedPath(bg, 0, 0, w, h, [0, 0, brRadius, blRadius]);
|
|
126
|
+
bg.fill({ color: '#ffffff', alpha: 0.12 }); // ≈ filter:brightness(1.08), a touch stronger
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
draw(false);
|
|
130
|
+
const label = makeText(a.label, {
|
|
131
|
+
size: this.em,
|
|
132
|
+
weight: '800',
|
|
133
|
+
color: a.disabled ? '#3a3f47' : ink,
|
|
134
|
+
letterSpacing: this.em * 0.04,
|
|
135
|
+
upper: true,
|
|
136
|
+
align: 'center',
|
|
137
|
+
});
|
|
138
|
+
label.anchor.set(0.5);
|
|
139
|
+
label.position.set(w / 2, h / 2);
|
|
140
|
+
root.addChild(bg, label);
|
|
141
|
+
if (!a.disabled) {
|
|
142
|
+
root.eventMode = 'static';
|
|
143
|
+
root.cursor = 'pointer';
|
|
144
|
+
root.hitArea = new Rectangle(0, 0, w, h);
|
|
145
|
+
attachHover(root, () => draw(true), () => draw(false));
|
|
146
|
+
root.on('pointertap', () => a.onTap());
|
|
147
|
+
}
|
|
148
|
+
return root;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private addClose(onClose: () => void): void {
|
|
152
|
+
const size = 36;
|
|
153
|
+
const root = new Container();
|
|
154
|
+
const bg = new Graphics();
|
|
155
|
+
const draw = (hover: boolean): void => {
|
|
156
|
+
bg.clear();
|
|
157
|
+
bg.circle(size / 2, size / 2, size / 2);
|
|
158
|
+
bg.fill(hover ? this.host.tokens.plaqueGlass : this.host.tokens.plaqueDark);
|
|
159
|
+
};
|
|
160
|
+
draw(false);
|
|
161
|
+
const glyph = makeIcon('close', 20, '#ffffff');
|
|
162
|
+
glyph.position.set((size - 20) / 2, (size - 20) / 2);
|
|
163
|
+
root.addChild(bg, glyph);
|
|
164
|
+
root.eventMode = 'static';
|
|
165
|
+
root.cursor = 'pointer';
|
|
166
|
+
root.hitArea = new Rectangle(0, 0, size, size);
|
|
167
|
+
attachHover(root, () => {
|
|
168
|
+
draw(true);
|
|
169
|
+
glyph.setColor(this.host.tokens.accent);
|
|
170
|
+
}, () => {
|
|
171
|
+
draw(false);
|
|
172
|
+
glyph.setColor('#ffffff');
|
|
173
|
+
});
|
|
174
|
+
attachPress(root, 0.92, onClose);
|
|
175
|
+
this.closeBtn = root;
|
|
176
|
+
this.addChild(root); // pinned to the screen corner, not the card
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Measure body + actions, draw the card background/mask, then place everything. Call once
|
|
180
|
+
* after the body content and actions are set. */
|
|
181
|
+
build(): void {
|
|
182
|
+
this.body.setLayoutSize(this.cardW, undefined);
|
|
183
|
+
const bodyH = this.body.outerHeight;
|
|
184
|
+
this.cardH = bodyH + this.actionsH;
|
|
185
|
+
|
|
186
|
+
this.bg.clear();
|
|
187
|
+
this.bg.roundRect(0, 0, this.cardW, this.cardH, 1.3 * this.em);
|
|
188
|
+
this.bg.fill(this.host.tokens.plaqueSolid);
|
|
189
|
+
|
|
190
|
+
this.body.position.set(0, 0);
|
|
191
|
+
this.actionsRow.position.set(0, bodyH);
|
|
192
|
+
|
|
193
|
+
this.resize(this.host.screenW, this.host.screenH);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
resize(w: number, h: number): void {
|
|
197
|
+
this.veil.clear();
|
|
198
|
+
this.veil.rect(0, 0, w, h);
|
|
199
|
+
this.veil.fill(this.host.tokens.backdrop);
|
|
200
|
+
this.veil.hitArea = new Rectangle(0, 0, w, h);
|
|
201
|
+
this.recentre(w, h);
|
|
202
|
+
this.fit();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private recentre(w: number, h: number): void {
|
|
206
|
+
this.cardRoot.position.set((w - this.cardW) / 2, (h - this.cardH) / 2);
|
|
207
|
+
if (this.closeBtn) this.closeBtn.position.set(w - 36 - 12, 12);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Scale the card down to fit a short/narrow popout (GameShell.fitSheet). */
|
|
211
|
+
fit(): void {
|
|
212
|
+
const availW = this.host.screenW;
|
|
213
|
+
const availH = this.host.screenH;
|
|
214
|
+
if (this.cardW <= 0 || this.cardH <= 0) return;
|
|
215
|
+
const s = Math.min(1, (availW * MODAL_FIT) / this.cardW, (availH * MODAL_FIT) / this.cardH);
|
|
216
|
+
this.cardRoot.scale.set(s < 0.999 ? s : 1);
|
|
217
|
+
// keep the scaled card centred
|
|
218
|
+
const sw = this.cardW * this.cardRoot.scale.x;
|
|
219
|
+
const sh = this.cardH * this.cardRoot.scale.y;
|
|
220
|
+
this.cardRoot.position.set((availW - sw) / 2, (availH - sh) / 2);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Card font-size knob — clamp(11px, 2cqmin, 15px) of the shell root. */
|
|
225
|
+
function clampEm(w: number, h: number): number {
|
|
226
|
+
return Math.max(11, Math.min(15, Math.min(w, h) * 0.02));
|
|
227
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Container, Graphics, Rectangle, Text, type FederatedPointerEvent } from 'pixi.js';
|
|
2
|
+
import type { PixiComponentContext } from '../context';
|
|
3
|
+
import { makeText } from '../text';
|
|
4
|
+
import { FlexBox, type Sizable } from './flex';
|
|
5
|
+
import { attachHover } from './widgets';
|
|
6
|
+
|
|
7
|
+
// Overlay/modal content primitives — section plaques, full-width glass rows, chips, sliders,
|
|
8
|
+
// wrapped paragraphs. Each mirrors a CSS class from the DOM shell (.ge-gi-sec, .ge-ov-row,
|
|
9
|
+
// .ge-chip, .ge-slider …).
|
|
10
|
+
|
|
11
|
+
/** Wrapped body text — `.ge-gi-sec p` / `.ge-gi-win-desc` etc. */
|
|
12
|
+
export function paragraph(
|
|
13
|
+
host: PixiComponentContext,
|
|
14
|
+
text: string,
|
|
15
|
+
width: number,
|
|
16
|
+
opts: { size?: number; color?: string } = {},
|
|
17
|
+
): Text {
|
|
18
|
+
return makeText(text, {
|
|
19
|
+
size: opts.size ?? 15,
|
|
20
|
+
weight: '400',
|
|
21
|
+
color: opts.color ?? 'rgba(255,255,255,.88)',
|
|
22
|
+
wrapWidth: width,
|
|
23
|
+
lineHeight: (opts.size ?? 15) * 1.6,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A zero-size flex child that grows to push siblings apart (CSS `flex:1` spacer). */
|
|
28
|
+
export class Spacer extends Container implements Sizable {
|
|
29
|
+
private w = 0;
|
|
30
|
+
setLayoutSize(w: number | undefined): void {
|
|
31
|
+
if (w != null) this.w = w;
|
|
32
|
+
}
|
|
33
|
+
measureSize(): { w: number; h: number } {
|
|
34
|
+
return { w: this.w, h: 0 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A titled glass-plaque section — `.ge-gi-sec`. Stretch to the body width; add content via `.add`. */
|
|
39
|
+
export function section(host: PixiComponentContext, title?: string): FlexBox {
|
|
40
|
+
const sec = new FlexBox({
|
|
41
|
+
direction: 'column',
|
|
42
|
+
align: 'start',
|
|
43
|
+
gap: 12,
|
|
44
|
+
padding: { top: 16, bottom: 16, left: 18, right: 18 },
|
|
45
|
+
background: { fill: host.tokens.plaqueGlass, radius: 16 },
|
|
46
|
+
});
|
|
47
|
+
if (title) {
|
|
48
|
+
sec.add(
|
|
49
|
+
makeText(title, {
|
|
50
|
+
size: 11,
|
|
51
|
+
weight: '700',
|
|
52
|
+
color: host.tokens.plaqueLabel,
|
|
53
|
+
letterSpacing: 11 * 0.14,
|
|
54
|
+
upper: true,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return sec;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A full-width glass row — `.ge-ov-row`. `button` adds hover (glass-hover bg + accent label). */
|
|
62
|
+
export class GlassRow extends Container implements Sizable {
|
|
63
|
+
private host: PixiComponentContext;
|
|
64
|
+
private bg = new Graphics();
|
|
65
|
+
private content = new Container();
|
|
66
|
+
private w = 0;
|
|
67
|
+
private h: number;
|
|
68
|
+
private padX = 16;
|
|
69
|
+
private isButton: boolean;
|
|
70
|
+
private hoverLabels: Text[] = [];
|
|
71
|
+
|
|
72
|
+
constructor(host: PixiComponentContext, opts: { height?: number; button?: boolean; onTap?: () => void } = {}) {
|
|
73
|
+
super();
|
|
74
|
+
this.host = host;
|
|
75
|
+
this.h = opts.height ?? 46;
|
|
76
|
+
this.isButton = !!opts.button;
|
|
77
|
+
this.addChild(this.bg, this.content);
|
|
78
|
+
if (opts.button) {
|
|
79
|
+
this.eventMode = 'static';
|
|
80
|
+
this.cursor = 'pointer';
|
|
81
|
+
attachHover(this, () => this.paint(true), () => this.paint(false));
|
|
82
|
+
if (opts.onTap) this.on('pointertap', opts.onTap);
|
|
83
|
+
}
|
|
84
|
+
this.paint(false);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Register a label to recolour to accent on hover (button rows). */
|
|
88
|
+
addContent(node: Container, accentOnHover?: Text): this {
|
|
89
|
+
this.content.addChild(node);
|
|
90
|
+
if (accentOnHover) this.hoverLabels.push(accentOnHover);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private paint(hover: boolean): void {
|
|
95
|
+
this.bg.clear();
|
|
96
|
+
this.bg.roundRect(0, 0, this.w || 1, this.h, 16);
|
|
97
|
+
this.bg.fill(hover && this.isButton ? this.host.tokens.plaqueGlassHover : this.host.tokens.plaqueGlass);
|
|
98
|
+
for (const l of this.hoverLabels) l.style.fill = hover ? this.host.tokens.accent : '#ffffff';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setLayoutSize(w: number | undefined): void {
|
|
102
|
+
if (w != null) this.w = w;
|
|
103
|
+
this.paint(false);
|
|
104
|
+
this.hitArea = new Rectangle(0, 0, this.w, this.h);
|
|
105
|
+
}
|
|
106
|
+
measureSize(): { w: number; h: number } {
|
|
107
|
+
return { w: this.w, h: this.h };
|
|
108
|
+
}
|
|
109
|
+
get innerWidth(): number {
|
|
110
|
+
return this.w - this.padX * 2;
|
|
111
|
+
}
|
|
112
|
+
get rowHeight(): number {
|
|
113
|
+
return this.h;
|
|
114
|
+
}
|
|
115
|
+
get pad(): number {
|
|
116
|
+
return this.padX;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Draggable volume slider — `.ge-slider` (accent fill + white thumb). */
|
|
121
|
+
export class Slider extends Container implements Sizable {
|
|
122
|
+
private host: PixiComponentContext;
|
|
123
|
+
private track = new Graphics();
|
|
124
|
+
private fill = new Graphics();
|
|
125
|
+
private thumb = new Graphics();
|
|
126
|
+
private w = 0;
|
|
127
|
+
private _value: number;
|
|
128
|
+
private onInput: (v: number) => void;
|
|
129
|
+
|
|
130
|
+
constructor(host: PixiComponentContext, value: number, onInput: (v: number) => void) {
|
|
131
|
+
super();
|
|
132
|
+
this.host = host;
|
|
133
|
+
this._value = value;
|
|
134
|
+
this.onInput = onInput;
|
|
135
|
+
this.addChild(this.track, this.fill, this.thumb);
|
|
136
|
+
this.eventMode = 'static';
|
|
137
|
+
this.cursor = 'pointer';
|
|
138
|
+
this.on('pointerdown', this.onDown);
|
|
139
|
+
this.on('globalpointermove', this.onMove);
|
|
140
|
+
// release over the slider (pointerup) OR anywhere else after a press on it (pointerupoutside)
|
|
141
|
+
this.on('pointerup', this.endDrag);
|
|
142
|
+
this.on('pointerupoutside', this.endDrag);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private dragging = false;
|
|
146
|
+
private onDown = (e: FederatedPointerEvent): void => {
|
|
147
|
+
this.dragging = true;
|
|
148
|
+
this.setFromX(this.toLocal(e.global).x);
|
|
149
|
+
};
|
|
150
|
+
private onMove = (e: FederatedPointerEvent): void => {
|
|
151
|
+
if (!this.dragging) return;
|
|
152
|
+
this.setFromX(this.toLocal(e.global).x);
|
|
153
|
+
};
|
|
154
|
+
private endDrag = (): void => {
|
|
155
|
+
this.dragging = false;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Thumb radius — the thumb travels within [R, w−R] so it never overhangs the track ends (like a
|
|
159
|
+
// native range input), giving even left/right padding instead of poking past the right edge.
|
|
160
|
+
private static R = 9;
|
|
161
|
+
|
|
162
|
+
private setFromX(x: number): void {
|
|
163
|
+
const usable = this.w - 2 * Slider.R;
|
|
164
|
+
const v = Math.max(0, Math.min(1, usable > 0 ? (x - Slider.R) / usable : 0));
|
|
165
|
+
this._value = v;
|
|
166
|
+
this.draw();
|
|
167
|
+
this.onInput(v);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private draw(): void {
|
|
171
|
+
const h = 24;
|
|
172
|
+
const cy = h / 2;
|
|
173
|
+
const tr = 6; // track thickness (thicker, closer to the DOM accent-color range)
|
|
174
|
+
const r = Slider.R;
|
|
175
|
+
const cx = r + this._value * Math.max(0, this.w - 2 * r); // thumb centre, kept within the track
|
|
176
|
+
this.track.clear();
|
|
177
|
+
this.track.roundRect(0, cy - tr / 2, this.w, tr, tr / 2).fill(this.host.tokens.track);
|
|
178
|
+
this.fill.clear();
|
|
179
|
+
this.fill.roundRect(0, cy - tr / 2, cx, tr, tr / 2).fill(this.host.tokens.accent);
|
|
180
|
+
this.thumb.clear();
|
|
181
|
+
this.thumb.circle(cx, cy, r).fill('#ffffff');
|
|
182
|
+
this.hitArea = new Rectangle(0, 0, this.w, h);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setLayoutSize(w: number | undefined): void {
|
|
186
|
+
if (w != null) this.w = w;
|
|
187
|
+
this.draw();
|
|
188
|
+
}
|
|
189
|
+
measureSize(): { w: number; h: number } {
|
|
190
|
+
return { w: this.w, h: 24 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Picker chip — `.ge-chip`. Selected = accent bg + accent border. */
|
|
195
|
+
export class Chip extends Container implements Sizable {
|
|
196
|
+
readonly id: string;
|
|
197
|
+
private host: PixiComponentContext;
|
|
198
|
+
private bg = new Graphics();
|
|
199
|
+
private labelText: Text;
|
|
200
|
+
private _on: boolean;
|
|
201
|
+
private w = 0;
|
|
202
|
+
private h = 0;
|
|
203
|
+
private em: number;
|
|
204
|
+
|
|
205
|
+
constructor(host: PixiComponentContext, id: string, label: string, selected: boolean, em: number, onTap: (id: string) => void) {
|
|
206
|
+
super();
|
|
207
|
+
this.id = id;
|
|
208
|
+
this.host = host;
|
|
209
|
+
this._on = selected;
|
|
210
|
+
this.em = em;
|
|
211
|
+
this.labelText = makeText(label, { size: em, weight: '700', color: '#ffffff', align: 'center' });
|
|
212
|
+
this.addChild(this.bg, this.labelText);
|
|
213
|
+
this.eventMode = 'static';
|
|
214
|
+
this.cursor = 'pointer';
|
|
215
|
+
this.on('pointertap', () => onTap(id));
|
|
216
|
+
attachHover(this, () => this.paint(true), () => this.paint(false));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setSelected(v: boolean): void {
|
|
220
|
+
this._on = v;
|
|
221
|
+
this.paint(false);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private paint(hover: boolean): void {
|
|
225
|
+
const t = this.host.tokens;
|
|
226
|
+
this.bg.clear();
|
|
227
|
+
this.bg.roundRect(0, 0, this.w, this.h, 0.8 * this.em);
|
|
228
|
+
this.bg.fill(this._on ? t.accent : hover ? t.plaqueGlassHover : 'rgba(255,255,255,.04)');
|
|
229
|
+
this.bg.stroke({ color: this._on ? t.accent : t.plaqueLine, width: 1 });
|
|
230
|
+
this.labelText.position.set((this.w - this.labelText.width) / 2, (this.h - this.labelText.height) / 2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setLayoutSize(w: number | undefined, h: number | undefined): void {
|
|
234
|
+
if (w != null) this.w = w;
|
|
235
|
+
// height from content (padding .8em vertical) unless imposed
|
|
236
|
+
this.h = h ?? this.labelText.height + 1.6 * this.em;
|
|
237
|
+
this.paint(false);
|
|
238
|
+
this.hitArea = new Rectangle(0, 0, this.w, this.h);
|
|
239
|
+
}
|
|
240
|
+
measureSize(): { w: number; h: number } {
|
|
241
|
+
return { w: this.w || this.labelText.width + 1.1 * this.em, h: this.h || this.labelText.height + 1.6 * this.em };
|
|
242
|
+
}
|
|
243
|
+
}
|