@energy8platform/game-engine 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +18 -3
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +18 -3
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +18 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +3 -5
- package/dist/host.esm.js +18 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +18 -2535
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -1020
- package/dist/index.esm.js +20 -2525
- package/dist/index.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -61
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -28
- package/dist/slot.esm.js +1857 -62
- package/dist/slot.esm.js.map +1 -1
- package/dist/vite.cjs.js +0 -5
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +0 -5
- package/dist/vite.esm.js.map +1 -1
- package/package.json +2 -35
- package/src/animation/Tween.ts +14 -3
- package/src/host/index.ts +0 -1
- package/src/host/types.ts +0 -3
- package/src/index.ts +0 -28
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -2
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
- package/src/vite/index.ts +0 -5
- package/dist/react-jsx.cjs.js +0 -3
- package/dist/react-jsx.cjs.js.map +0 -1
- package/dist/react-jsx.d.ts +0 -602
- package/dist/react-jsx.esm.js +0 -2
- package/dist/react-jsx.esm.js.map +0 -1
- package/dist/react.cjs.js +0 -3787
- package/dist/react.cjs.js.map +0 -1
- package/dist/react.d.ts +0 -1467
- package/dist/react.esm.js +0 -3771
- package/dist/react.esm.js.map +0 -1
- package/dist/ui.cjs.js +0 -2999
- package/dist/ui.cjs.js.map +0 -1
- package/dist/ui.d.ts +0 -1116
- package/dist/ui.esm.js +0 -2983
- package/dist/ui.esm.js.map +0 -1
- package/src/react/EngineContext.ts +0 -26
- package/src/react/ReactScene.ts +0 -88
- package/src/react/applyProps.ts +0 -271
- package/src/react/catalogue.ts +0 -17
- package/src/react/createPixiRoot.ts +0 -31
- package/src/react/extendAll.ts +0 -74
- package/src/react/hooks.ts +0 -46
- package/src/react/index.ts +0 -29
- package/src/react/jsx-runtime.ts +0 -346
- package/src/react/reconciler.ts +0 -338
- package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
- package/src/state/StateMachine.ts +0 -231
- package/src/state/index.ts +0 -1
- package/src/ui/BalanceDisplay.ts +0 -159
- package/src/ui/Button.ts +0 -304
- package/src/ui/FlexContainer.ts +0 -775
- package/src/ui/Label.ts +0 -124
- package/src/ui/LabelValue.ts +0 -122
- package/src/ui/Layout.ts +0 -291
- package/src/ui/Modal.ts +0 -170
- package/src/ui/Panel.ts +0 -204
- package/src/ui/ProgressBar.ts +0 -170
- package/src/ui/ScrollContainer.ts +0 -478
- package/src/ui/Slider.ts +0 -241
- package/src/ui/Toast.ts +0 -150
- package/src/ui/Toggle.ts +0 -201
- package/src/ui/WinDisplay.ts +0 -145
- package/src/ui/index.ts +0 -33
- package/src/ui/view.ts +0 -28
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import { Container, Graphics, type ColorSource, Ticker } from 'pixi.js';
|
|
2
|
-
import { resolveView } from './view';
|
|
3
|
-
import type { ViewInput } from './view';
|
|
4
|
-
|
|
5
|
-
// ─── Types ───────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
|
|
8
|
-
|
|
9
|
-
export interface ScrollContainerConfig {
|
|
10
|
-
/** Visible viewport width */
|
|
11
|
-
width: number;
|
|
12
|
-
|
|
13
|
-
/** Visible viewport height */
|
|
14
|
-
height: number;
|
|
15
|
-
|
|
16
|
-
/** Scroll direction (default: 'vertical') */
|
|
17
|
-
direction?: ScrollDirection;
|
|
18
|
-
|
|
19
|
-
/** Background color (undefined = transparent) */
|
|
20
|
-
backgroundColor?: ColorSource;
|
|
21
|
-
|
|
22
|
-
/** Border radius for the mask (default: 0) */
|
|
23
|
-
borderRadius?: number;
|
|
24
|
-
|
|
25
|
-
/** Gap between items (default: 0) */
|
|
26
|
-
elementsMargin?: number;
|
|
27
|
-
|
|
28
|
-
/** Padding */
|
|
29
|
-
padding?: number;
|
|
30
|
-
|
|
31
|
-
/** Disable easing/inertia */
|
|
32
|
-
disableEasing?: boolean;
|
|
33
|
-
|
|
34
|
-
/** Show scrollbar indicator (default: false) */
|
|
35
|
-
scrollbar?: boolean;
|
|
36
|
-
|
|
37
|
-
/** Custom scrollbar thumb view (string texture name, Texture, or Container) */
|
|
38
|
-
thumbView?: ViewInput;
|
|
39
|
-
|
|
40
|
-
/** Scrollbar width in px (default: 6) */
|
|
41
|
-
scrollbarWidth?: number;
|
|
42
|
-
|
|
43
|
-
/** Scrollbar padding from edge (default: 4) */
|
|
44
|
-
scrollbarPadding?: number;
|
|
45
|
-
|
|
46
|
-
/** Scrollbar color (when no thumbView, default: 0xaaaaaa) */
|
|
47
|
-
scrollbarColor?: number;
|
|
48
|
-
|
|
49
|
-
/** Scrollbar alpha (default: 0.5) */
|
|
50
|
-
scrollbarAlpha?: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const DECELERATION = 0.95;
|
|
54
|
-
const MIN_VELOCITY = 0.5;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Scrollable container with touch/drag, mouse wheel, and inertia.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```ts
|
|
61
|
-
* const scroll = new ScrollContainer({
|
|
62
|
-
* width: 600,
|
|
63
|
-
* height: 400,
|
|
64
|
-
* direction: 'vertical',
|
|
65
|
-
* elementsMargin: 8,
|
|
66
|
-
* });
|
|
67
|
-
*
|
|
68
|
-
* for (let i = 0; i < 50; i++) {
|
|
69
|
-
* scroll.addItem(createRow(i));
|
|
70
|
-
* }
|
|
71
|
-
*
|
|
72
|
-
* scene.container.addChild(scroll);
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
export class ScrollContainer extends Container {
|
|
76
|
-
readonly __uiComponent = true as const;
|
|
77
|
-
|
|
78
|
-
private _viewport: { width: number; height: number };
|
|
79
|
-
private _internalSetup = true;
|
|
80
|
-
private _content: Container;
|
|
81
|
-
private _maskGfx: Graphics;
|
|
82
|
-
private _bg: Graphics | null = null;
|
|
83
|
-
private _scrollConfig: Required<Pick<ScrollContainerConfig, 'direction' | 'elementsMargin' | 'padding' | 'borderRadius' | 'disableEasing'>>;
|
|
84
|
-
private _items: Container[] = [];
|
|
85
|
-
|
|
86
|
-
// Scrollbar
|
|
87
|
-
private _scrollbar: Container | null = null;
|
|
88
|
-
private _scrollbarConfig: { width: number; padding: number };
|
|
89
|
-
|
|
90
|
-
// Drag state
|
|
91
|
-
private _dragging = false;
|
|
92
|
-
private _dragStart = { x: 0, y: 0 };
|
|
93
|
-
private _contentStart = { x: 0, y: 0 };
|
|
94
|
-
private _velocity = { x: 0, y: 0 };
|
|
95
|
-
private _lastDragPos = { x: 0, y: 0 };
|
|
96
|
-
private _lastDragTime = 0;
|
|
97
|
-
private _inertiaActive = false;
|
|
98
|
-
|
|
99
|
-
// Bound handlers for cleanup
|
|
100
|
-
private _onTickBound: ((ticker: Ticker) => void) | null = null;
|
|
101
|
-
private _onWheelBound: ((e: WheelEvent) => void) | null = null;
|
|
102
|
-
|
|
103
|
-
constructor(config: ScrollContainerConfig) {
|
|
104
|
-
super();
|
|
105
|
-
|
|
106
|
-
this._viewport = { width: config.width, height: config.height };
|
|
107
|
-
this._scrollConfig = {
|
|
108
|
-
direction: config.direction ?? 'vertical',
|
|
109
|
-
elementsMargin: config.elementsMargin ?? 0,
|
|
110
|
-
padding: config.padding ?? 0,
|
|
111
|
-
borderRadius: config.borderRadius ?? 0,
|
|
112
|
-
disableEasing: config.disableEasing ?? false,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Background
|
|
116
|
-
if (config.backgroundColor !== undefined) {
|
|
117
|
-
this._bg = new Graphics();
|
|
118
|
-
this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
119
|
-
.fill(config.backgroundColor);
|
|
120
|
-
this.addChild(this._bg);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Mask
|
|
124
|
-
this._maskGfx = new Graphics();
|
|
125
|
-
this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
|
|
126
|
-
.fill(0xffffff);
|
|
127
|
-
this.addChild(this._maskGfx);
|
|
128
|
-
|
|
129
|
-
// Content container
|
|
130
|
-
this._content = new Container();
|
|
131
|
-
this._content.mask = this._maskGfx;
|
|
132
|
-
this.addChild(this._content);
|
|
133
|
-
|
|
134
|
-
// Interaction
|
|
135
|
-
this.eventMode = 'static';
|
|
136
|
-
this.hitArea = { contains: (x: number, y: number) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
|
|
137
|
-
|
|
138
|
-
this.on('pointerdown', this._onPointerDown, this);
|
|
139
|
-
this.on('pointermove', this._onPointerMove, this);
|
|
140
|
-
this.on('pointerup', this._onPointerUp, this);
|
|
141
|
-
this.on('pointerupoutside', this._onPointerUp, this);
|
|
142
|
-
|
|
143
|
-
// Mouse wheel
|
|
144
|
-
this._onWheelBound = this._onWheel.bind(this);
|
|
145
|
-
|
|
146
|
-
// Scrollbar
|
|
147
|
-
const sbWidth = config.scrollbarWidth ?? 6;
|
|
148
|
-
const sbPadding = config.scrollbarPadding ?? 4;
|
|
149
|
-
this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
|
|
150
|
-
|
|
151
|
-
if (config.scrollbar) {
|
|
152
|
-
const customThumb = resolveView(config.thumbView);
|
|
153
|
-
if (customThumb) {
|
|
154
|
-
this._scrollbar = customThumb;
|
|
155
|
-
} else {
|
|
156
|
-
const g = new Graphics();
|
|
157
|
-
g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
|
|
158
|
-
g.alpha = config.scrollbarAlpha ?? 0.5;
|
|
159
|
-
this._scrollbar = g;
|
|
160
|
-
}
|
|
161
|
-
this._scrollbar.visible = false;
|
|
162
|
-
super.addChild(this._scrollbar);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
this._internalSetup = false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Override addChild so external children are routed to scroll content.
|
|
170
|
-
* Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
|
|
171
|
-
*/
|
|
172
|
-
override addChild<T extends Container>(...children: T[]): T {
|
|
173
|
-
if (this._internalSetup) {
|
|
174
|
-
return super.addChild(...children);
|
|
175
|
-
}
|
|
176
|
-
for (const child of children) {
|
|
177
|
-
this.addItem(child as any);
|
|
178
|
-
}
|
|
179
|
-
return children[0];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
override removeChild<T extends Container>(...children: T[]): T {
|
|
183
|
-
if (this._internalSetup) {
|
|
184
|
-
return super.removeChild(...children);
|
|
185
|
-
}
|
|
186
|
-
for (const child of children) {
|
|
187
|
-
const idx = this._items.indexOf(child as any);
|
|
188
|
-
if (idx !== -1) {
|
|
189
|
-
this._items.splice(idx, 1);
|
|
190
|
-
this._content.removeChild(child);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
this.layoutItems();
|
|
194
|
-
return children[0];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** React reconciler update hook */
|
|
198
|
-
updateConfig(changed: Record<string, any>): void {
|
|
199
|
-
if ('width' in changed || 'height' in changed) {
|
|
200
|
-
this.setViewportSize(
|
|
201
|
-
changed.width ?? this._viewport.width,
|
|
202
|
-
changed.height ?? this._viewport.height,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Enable mouse wheel scrolling (call after adding to stage) */
|
|
208
|
-
enableWheel(canvas: HTMLCanvasElement): void {
|
|
209
|
-
if (this._onWheelBound) {
|
|
210
|
-
canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Set scrollable content. Replaces any existing items. */
|
|
215
|
-
setContent(content: Container): void {
|
|
216
|
-
this.clearItems();
|
|
217
|
-
const children = [...content.children] as Container[];
|
|
218
|
-
for (const child of children) {
|
|
219
|
-
this.addItem(child);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Add a single item */
|
|
224
|
-
addItem(child: Container): this {
|
|
225
|
-
this._items.push(child);
|
|
226
|
-
this._content.addChild(child);
|
|
227
|
-
this.layoutItems();
|
|
228
|
-
return this;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Remove all items */
|
|
232
|
-
clearItems(): void {
|
|
233
|
-
for (const item of this._items) {
|
|
234
|
-
this._content.removeChild(item);
|
|
235
|
-
}
|
|
236
|
-
this._items.length = 0;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Get items */
|
|
240
|
-
get items(): readonly Container[] {
|
|
241
|
-
return this._items;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Scroll to make a specific item index visible */
|
|
245
|
-
scrollToItem(index: number): void {
|
|
246
|
-
if (index < 0 || index >= this._items.length) return;
|
|
247
|
-
const item = this._items[index];
|
|
248
|
-
const isVert = this._scrollConfig.direction !== 'horizontal';
|
|
249
|
-
|
|
250
|
-
if (isVert) {
|
|
251
|
-
this._content.y = -item.y + this._scrollConfig.padding;
|
|
252
|
-
} else {
|
|
253
|
-
this._content.x = -item.x + this._scrollConfig.padding;
|
|
254
|
-
}
|
|
255
|
-
this.clampScroll();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Current scroll position */
|
|
259
|
-
get scrollPosition(): { x: number; y: number } {
|
|
260
|
-
return { x: this._content.x, y: this._content.y };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Resize the scroll viewport */
|
|
264
|
-
setViewportSize(width: number, height: number): void {
|
|
265
|
-
this._viewport.width = width;
|
|
266
|
-
this._viewport.height = height;
|
|
267
|
-
|
|
268
|
-
this._maskGfx.clear();
|
|
269
|
-
this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
|
|
270
|
-
|
|
271
|
-
if (this._bg) {
|
|
272
|
-
this._bg.clear();
|
|
273
|
-
this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
|
|
274
|
-
.fill(0xffffff); // color will be overridden if needed
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
this.clampScroll();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ─── Layout ──────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
private layoutItems(): void {
|
|
283
|
-
const { direction, elementsMargin, padding } = this._scrollConfig;
|
|
284
|
-
const isVert = direction !== 'horizontal';
|
|
285
|
-
let pos = padding;
|
|
286
|
-
|
|
287
|
-
for (const item of this._items) {
|
|
288
|
-
if (isVert) {
|
|
289
|
-
item.x = padding;
|
|
290
|
-
item.y = pos;
|
|
291
|
-
pos += item.height + elementsMargin;
|
|
292
|
-
} else {
|
|
293
|
-
item.x = pos;
|
|
294
|
-
item.y = padding;
|
|
295
|
-
pos += item.width + elementsMargin;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ─── Drag handling ───────────────────────────────────
|
|
301
|
-
|
|
302
|
-
private _onPointerDown(e: import('pixi.js').FederatedPointerEvent): void {
|
|
303
|
-
this._dragging = true;
|
|
304
|
-
this._inertiaActive = false;
|
|
305
|
-
this._dragStart.x = e.globalX;
|
|
306
|
-
this._dragStart.y = e.globalY;
|
|
307
|
-
this._contentStart.x = this._content.x;
|
|
308
|
-
this._contentStart.y = this._content.y;
|
|
309
|
-
this._lastDragPos.x = e.globalX;
|
|
310
|
-
this._lastDragPos.y = e.globalY;
|
|
311
|
-
this._lastDragTime = Date.now();
|
|
312
|
-
this._velocity.x = 0;
|
|
313
|
-
this._velocity.y = 0;
|
|
314
|
-
|
|
315
|
-
this.stopInertia();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
private _onPointerMove(e: import('pixi.js').FederatedPointerEvent): void {
|
|
319
|
-
if (!this._dragging) return;
|
|
320
|
-
|
|
321
|
-
const dx = e.globalX - this._dragStart.x;
|
|
322
|
-
const dy = e.globalY - this._dragStart.y;
|
|
323
|
-
const { direction } = this._scrollConfig;
|
|
324
|
-
|
|
325
|
-
if (direction !== 'horizontal') {
|
|
326
|
-
this._content.y = this._contentStart.y + dy;
|
|
327
|
-
}
|
|
328
|
-
if (direction !== 'vertical') {
|
|
329
|
-
this._content.x = this._contentStart.x + dx;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Track velocity
|
|
333
|
-
const now = Date.now();
|
|
334
|
-
const dt = now - this._lastDragTime;
|
|
335
|
-
if (dt > 0) {
|
|
336
|
-
this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
|
|
337
|
-
this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
|
|
338
|
-
}
|
|
339
|
-
this._lastDragPos.x = e.globalX;
|
|
340
|
-
this._lastDragPos.y = e.globalY;
|
|
341
|
-
this._lastDragTime = now;
|
|
342
|
-
|
|
343
|
-
this.clampScroll();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private _onPointerUp(): void {
|
|
347
|
-
if (!this._dragging) return;
|
|
348
|
-
this._dragging = false;
|
|
349
|
-
|
|
350
|
-
if (!this._scrollConfig.disableEasing &&
|
|
351
|
-
(Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
|
|
352
|
-
this.startInertia();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ─── Inertia ─────────────────────────────────────────
|
|
357
|
-
|
|
358
|
-
private startInertia(): void {
|
|
359
|
-
this._inertiaActive = true;
|
|
360
|
-
this._onTickBound = this._inertiaTick.bind(this);
|
|
361
|
-
Ticker.shared.add(this._onTickBound);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private stopInertia(): void {
|
|
365
|
-
if (this._onTickBound && this._inertiaActive) {
|
|
366
|
-
Ticker.shared.remove(this._onTickBound);
|
|
367
|
-
this._inertiaActive = false;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
private _inertiaTick(): void {
|
|
372
|
-
const { direction } = this._scrollConfig;
|
|
373
|
-
|
|
374
|
-
if (direction !== 'horizontal') {
|
|
375
|
-
this._content.y += this._velocity.y;
|
|
376
|
-
this._velocity.y *= DECELERATION;
|
|
377
|
-
}
|
|
378
|
-
if (direction !== 'vertical') {
|
|
379
|
-
this._content.x += this._velocity.x;
|
|
380
|
-
this._velocity.x *= DECELERATION;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
this.clampScroll();
|
|
384
|
-
|
|
385
|
-
if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
|
|
386
|
-
this.stopInertia();
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// ─── Mouse wheel ─────────────────────────────────────
|
|
391
|
-
|
|
392
|
-
private _onWheel(e: WheelEvent): void {
|
|
393
|
-
const { direction } = this._scrollConfig;
|
|
394
|
-
e.preventDefault();
|
|
395
|
-
|
|
396
|
-
if (direction !== 'horizontal') {
|
|
397
|
-
this._content.y -= e.deltaY;
|
|
398
|
-
}
|
|
399
|
-
if (direction !== 'vertical') {
|
|
400
|
-
this._content.x -= e.deltaX;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
this.clampScroll();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ─── Scroll bounds ───────────────────────────────────
|
|
407
|
-
|
|
408
|
-
private clampScroll(): void {
|
|
409
|
-
const { direction } = this._scrollConfig;
|
|
410
|
-
const bounds = this._content.getLocalBounds();
|
|
411
|
-
|
|
412
|
-
if (direction !== 'horizontal') {
|
|
413
|
-
const contentHeight = bounds.height + bounds.y;
|
|
414
|
-
const maxScroll = Math.min(0, this._viewport.height - contentHeight);
|
|
415
|
-
this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (direction !== 'vertical') {
|
|
419
|
-
const contentWidth = bounds.width + bounds.x;
|
|
420
|
-
const maxScroll = Math.min(0, this._viewport.width - contentWidth);
|
|
421
|
-
this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
this.updateScrollbar();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private updateScrollbar(): void {
|
|
428
|
-
if (!this._scrollbar) return;
|
|
429
|
-
const { direction } = this._scrollConfig;
|
|
430
|
-
const { width: sbW, padding: sbPad } = this._scrollbarConfig;
|
|
431
|
-
const bounds = this._content.getLocalBounds();
|
|
432
|
-
const isVert = direction !== 'horizontal';
|
|
433
|
-
|
|
434
|
-
if (isVert) {
|
|
435
|
-
const contentH = bounds.height + bounds.y;
|
|
436
|
-
if (contentH <= this._viewport.height) {
|
|
437
|
-
this._scrollbar.visible = false;
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
this._scrollbar.visible = true;
|
|
441
|
-
const ratio = this._viewport.height / contentH;
|
|
442
|
-
const thumbH = Math.max(20, this._viewport.height * ratio);
|
|
443
|
-
const scrollRange = this._viewport.height - thumbH;
|
|
444
|
-
const scrollProgress = -this._content.y / (contentH - this._viewport.height);
|
|
445
|
-
|
|
446
|
-
this._scrollbar.x = this._viewport.width - sbW - sbPad;
|
|
447
|
-
this._scrollbar.y = scrollProgress * scrollRange;
|
|
448
|
-
this._scrollbar.height = thumbH;
|
|
449
|
-
this._scrollbar.width = sbW;
|
|
450
|
-
} else {
|
|
451
|
-
const contentW = bounds.width + bounds.x;
|
|
452
|
-
if (contentW <= this._viewport.width) {
|
|
453
|
-
this._scrollbar.visible = false;
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
this._scrollbar.visible = true;
|
|
457
|
-
const ratio = this._viewport.width / contentW;
|
|
458
|
-
const thumbW = Math.max(20, this._viewport.width * ratio);
|
|
459
|
-
const scrollRange = this._viewport.width - thumbW;
|
|
460
|
-
const scrollProgress = -this._content.x / (contentW - this._viewport.width);
|
|
461
|
-
|
|
462
|
-
this._scrollbar.y = this._viewport.height - sbW - sbPad;
|
|
463
|
-
this._scrollbar.x = scrollProgress * scrollRange;
|
|
464
|
-
this._scrollbar.width = thumbW;
|
|
465
|
-
this._scrollbar.height = sbW;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
470
|
-
this.stopInertia();
|
|
471
|
-
this.off('pointerdown', this._onPointerDown, this);
|
|
472
|
-
this.off('pointermove', this._onPointerMove, this);
|
|
473
|
-
this.off('pointerup', this._onPointerUp, this);
|
|
474
|
-
this.off('pointerupoutside', this._onPointerUp, this);
|
|
475
|
-
this._items.length = 0;
|
|
476
|
-
super.destroy(options);
|
|
477
|
-
}
|
|
478
|
-
}
|
package/src/ui/Slider.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { Container, Graphics, FederatedPointerEvent } from 'pixi.js';
|
|
2
|
-
import { resolveView } from './view';
|
|
3
|
-
import type { ViewInput } from './view';
|
|
4
|
-
|
|
5
|
-
export interface SliderConfig {
|
|
6
|
-
/** Minimum value (default: 0) */
|
|
7
|
-
min?: number;
|
|
8
|
-
/** Maximum value (default: 1) */
|
|
9
|
-
max?: number;
|
|
10
|
-
/** Step increment (0 = continuous, default: 0) */
|
|
11
|
-
step?: number;
|
|
12
|
-
/** Initial value (default: min) */
|
|
13
|
-
value?: number;
|
|
14
|
-
/** Track width in pixels (default: 200) */
|
|
15
|
-
width?: number;
|
|
16
|
-
/** Track height in pixels (default: 8) */
|
|
17
|
-
height?: number;
|
|
18
|
-
/** Corner radius for Graphics-based track/fill (default: 4) */
|
|
19
|
-
borderRadius?: number;
|
|
20
|
-
/** Track background color (ignored when trackView provided) */
|
|
21
|
-
trackColor?: number;
|
|
22
|
-
/** Fill bar color (ignored when fillView provided) */
|
|
23
|
-
fillColor?: number;
|
|
24
|
-
/** Handle radius (for Graphics-based handle, default: 12) */
|
|
25
|
-
handleRadius?: number;
|
|
26
|
-
/** Handle color (for Graphics-based handle, ignored when handleView provided) */
|
|
27
|
-
handleColor?: number;
|
|
28
|
-
|
|
29
|
-
/** Custom track background view */
|
|
30
|
-
trackView?: ViewInput;
|
|
31
|
-
/** Custom fill bar view */
|
|
32
|
-
fillView?: ViewInput;
|
|
33
|
-
/** Custom handle view */
|
|
34
|
-
handleView?: ViewInput;
|
|
35
|
-
|
|
36
|
-
/** Called when value changes during drag */
|
|
37
|
-
onUpdate?: (value: number) => void;
|
|
38
|
-
/** Called when drag ends */
|
|
39
|
-
onChange?: (value: number) => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Draggable slider with customizable track, fill, and handle views.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* const volume = new Slider({
|
|
48
|
-
* min: 0, max: 1, value: 0.5,
|
|
49
|
-
* width: 200, height: 8,
|
|
50
|
-
* fillColor: 0xffd700,
|
|
51
|
-
* onUpdate: (v) => console.log('Volume:', v),
|
|
52
|
-
* });
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export class Slider extends Container {
|
|
56
|
-
readonly __uiComponent = true as const;
|
|
57
|
-
|
|
58
|
-
private _track: Container;
|
|
59
|
-
private _fill: Container;
|
|
60
|
-
private _fillMask: Graphics;
|
|
61
|
-
private _handle: Container;
|
|
62
|
-
private _config: Required<Pick<SliderConfig, 'min' | 'max' | 'step' | 'width' | 'height' | 'borderRadius' | 'trackColor' | 'fillColor' | 'handleRadius' | 'handleColor'>>;
|
|
63
|
-
private _value: number;
|
|
64
|
-
private _dragging = false;
|
|
65
|
-
|
|
66
|
-
onUpdate: ((value: number) => void) | null = null;
|
|
67
|
-
onChange: ((value: number) => void) | null = null;
|
|
68
|
-
|
|
69
|
-
constructor(config: SliderConfig = {}) {
|
|
70
|
-
super();
|
|
71
|
-
|
|
72
|
-
this._config = {
|
|
73
|
-
min: config.min ?? 0,
|
|
74
|
-
max: config.max ?? 1,
|
|
75
|
-
step: config.step ?? 0,
|
|
76
|
-
width: config.width ?? 200,
|
|
77
|
-
height: config.height ?? 8,
|
|
78
|
-
borderRadius: config.borderRadius ?? 4,
|
|
79
|
-
trackColor: config.trackColor ?? 0x333333,
|
|
80
|
-
fillColor: config.fillColor ?? 0xffd700,
|
|
81
|
-
handleRadius: config.handleRadius ?? 12,
|
|
82
|
-
handleColor: config.handleColor ?? 0xffffff,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
this._value = config.value ?? this._config.min;
|
|
86
|
-
this.onUpdate = config.onUpdate ?? null;
|
|
87
|
-
this.onChange = config.onChange ?? null;
|
|
88
|
-
|
|
89
|
-
const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
|
|
90
|
-
|
|
91
|
-
// Track
|
|
92
|
-
const customTrack = resolveView(config.trackView);
|
|
93
|
-
if (customTrack) {
|
|
94
|
-
customTrack.width = width;
|
|
95
|
-
customTrack.height = height;
|
|
96
|
-
this._track = customTrack;
|
|
97
|
-
} else {
|
|
98
|
-
const g = new Graphics();
|
|
99
|
-
g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
|
|
100
|
-
this._track = g;
|
|
101
|
-
}
|
|
102
|
-
this.addChild(this._track);
|
|
103
|
-
|
|
104
|
-
// Fill
|
|
105
|
-
const customFill = resolveView(config.fillView);
|
|
106
|
-
if (customFill) {
|
|
107
|
-
customFill.width = width;
|
|
108
|
-
customFill.height = height;
|
|
109
|
-
this._fill = customFill;
|
|
110
|
-
} else {
|
|
111
|
-
const g = new Graphics();
|
|
112
|
-
g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
|
|
113
|
-
this._fill = g;
|
|
114
|
-
}
|
|
115
|
-
this.addChild(this._fill);
|
|
116
|
-
|
|
117
|
-
// Fill mask
|
|
118
|
-
this._fillMask = new Graphics();
|
|
119
|
-
this.addChild(this._fillMask);
|
|
120
|
-
this._fill.mask = this._fillMask;
|
|
121
|
-
|
|
122
|
-
// Handle
|
|
123
|
-
const customHandle = resolveView(config.handleView);
|
|
124
|
-
if (customHandle) {
|
|
125
|
-
this._handle = customHandle;
|
|
126
|
-
} else {
|
|
127
|
-
const g = new Graphics();
|
|
128
|
-
g.circle(0, 0, handleRadius).fill(handleColor);
|
|
129
|
-
this._handle = g;
|
|
130
|
-
}
|
|
131
|
-
this._handle.y = height / 2;
|
|
132
|
-
this.addChild(this._handle);
|
|
133
|
-
|
|
134
|
-
// Interaction
|
|
135
|
-
this.eventMode = 'static';
|
|
136
|
-
this.cursor = 'pointer';
|
|
137
|
-
|
|
138
|
-
// Hit area covers track + handle overflow
|
|
139
|
-
const hitPad = Math.max(handleRadius - height / 2, 0);
|
|
140
|
-
this.hitArea = { contains: (x: number, y: number) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
|
|
141
|
-
|
|
142
|
-
this.on('pointerdown', this._onPointerDown, this);
|
|
143
|
-
this.on('globalpointermove', this._onPointerMove, this);
|
|
144
|
-
this.on('pointerup', this._onPointerUp, this);
|
|
145
|
-
this.on('pointerupoutside', this._onPointerUp, this);
|
|
146
|
-
|
|
147
|
-
this._updateVisuals();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Current value */
|
|
151
|
-
get value(): number {
|
|
152
|
-
return this._value;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
set value(v: number) {
|
|
156
|
-
const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
|
|
157
|
-
if (clamped === this._value) return;
|
|
158
|
-
this._value = clamped;
|
|
159
|
-
this._updateVisuals();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
get min(): number { return this._config.min; }
|
|
163
|
-
get max(): number { return this._config.max; }
|
|
164
|
-
|
|
165
|
-
/** React reconciler update hook */
|
|
166
|
-
updateConfig(changed: Record<string, any>): void {
|
|
167
|
-
if ('value' in changed) this.value = changed.value;
|
|
168
|
-
if ('min' in changed) { this._config.min = changed.min; this._updateVisuals(); }
|
|
169
|
-
if ('max' in changed) { this._config.max = changed.max; this._updateVisuals(); }
|
|
170
|
-
if ('step' in changed) this._config.step = changed.step;
|
|
171
|
-
if ('onUpdate' in changed) this.onUpdate = changed.onUpdate;
|
|
172
|
-
if ('onChange' in changed) this.onChange = changed.onChange;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private _fraction(): number {
|
|
176
|
-
const { min, max } = this._config;
|
|
177
|
-
return max === min ? 0 : (this._value - min) / (max - min);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private _applyStep(v: number): number {
|
|
181
|
-
const { step, min } = this._config;
|
|
182
|
-
if (step <= 0) return v;
|
|
183
|
-
return min + Math.round((v - min) / step) * step;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private _updateVisuals(): void {
|
|
187
|
-
const frac = this._fraction();
|
|
188
|
-
const w = this._config.width;
|
|
189
|
-
const h = this._config.height;
|
|
190
|
-
|
|
191
|
-
// Update fill mask
|
|
192
|
-
this._fillMask.clear();
|
|
193
|
-
this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
|
|
194
|
-
|
|
195
|
-
// Update handle position
|
|
196
|
-
this._handle.x = w * frac;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private _valueFromPointer(e: FederatedPointerEvent): number {
|
|
200
|
-
const local = this.toLocal(e.global);
|
|
201
|
-
const frac = Math.max(0, Math.min(1, local.x / this._config.width));
|
|
202
|
-
const { min, max } = this._config;
|
|
203
|
-
return this._applyStep(min + frac * (max - min));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private _onPointerDown(e: FederatedPointerEvent): void {
|
|
207
|
-
this._dragging = true;
|
|
208
|
-
const newValue = this._valueFromPointer(e);
|
|
209
|
-
if (newValue !== this._value) {
|
|
210
|
-
this._value = newValue;
|
|
211
|
-
this._updateVisuals();
|
|
212
|
-
this.onUpdate?.(this._value);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private _onPointerMove(e: FederatedPointerEvent): void {
|
|
217
|
-
if (!this._dragging) return;
|
|
218
|
-
const newValue = this._valueFromPointer(e);
|
|
219
|
-
if (newValue !== this._value) {
|
|
220
|
-
this._value = newValue;
|
|
221
|
-
this._updateVisuals();
|
|
222
|
-
this.onUpdate?.(this._value);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private _onPointerUp(_e: FederatedPointerEvent): void {
|
|
227
|
-
if (!this._dragging) return;
|
|
228
|
-
this._dragging = false;
|
|
229
|
-
this.onChange?.(this._value);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
233
|
-
this.off('pointerdown', this._onPointerDown, this);
|
|
234
|
-
this.off('globalpointermove', this._onPointerMove, this);
|
|
235
|
-
this.off('pointerup', this._onPointerUp, this);
|
|
236
|
-
this.off('pointerupoutside', this._onPointerUp, this);
|
|
237
|
-
this.onUpdate = null;
|
|
238
|
-
this.onChange = null;
|
|
239
|
-
super.destroy(options);
|
|
240
|
-
}
|
|
241
|
-
}
|