@energy8platform/game-engine 0.11.0 → 0.13.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 +88 -1
- package/bin/simulate.ts +45 -5
- package/dist/index.cjs.js +49 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +49 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +237 -0
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +59 -2
- package/dist/lua.esm.js +236 -2
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +667 -215
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +667 -215
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +407 -7
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +156 -2
- package/dist/ui.esm.js +406 -8
- package/dist/ui.esm.js.map +1 -1
- package/package.json +4 -2
- package/scripts/install-simulate.mjs +100 -0
- package/src/lua/NativeSimulationRunner.ts +367 -0
- package/src/lua/index.ts +7 -0
- package/src/react/applyProps.ts +6 -4
- package/src/react/extendAll.ts +2 -0
- package/src/react/jsx.d.ts +28 -1
- package/src/react/reconciler.ts +58 -2
- package/src/ui/FlexContainer.ts +57 -7
- package/src/ui/Slider.ts +241 -0
- package/src/ui/Toggle.ts +201 -0
- package/src/ui/index.ts +5 -1
package/src/ui/FlexContainer.ts
CHANGED
|
@@ -6,13 +6,21 @@ export type FlexDirection = 'row' | 'column';
|
|
|
6
6
|
export type JustifyContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around';
|
|
7
7
|
export type AlignItems = 'start' | 'center' | 'end' | 'stretch';
|
|
8
8
|
|
|
9
|
+
export type AlignSelf = 'auto' | 'start' | 'center' | 'end' | 'stretch';
|
|
10
|
+
|
|
9
11
|
export interface FlexItemConfig {
|
|
10
12
|
/** Flex grow factor (0 = fixed size) */
|
|
11
13
|
flexGrow?: number;
|
|
14
|
+
/** Flex shrink factor (0 = don't shrink, default: 1) */
|
|
15
|
+
flexShrink?: number;
|
|
12
16
|
/** Explicit width override for layout calculations */
|
|
13
17
|
layoutWidth?: number;
|
|
14
18
|
/** Explicit height override for layout calculations */
|
|
15
19
|
layoutHeight?: number;
|
|
20
|
+
/** Override parent's alignItems for this child */
|
|
21
|
+
alignSelf?: AlignSelf;
|
|
22
|
+
/** Exclude from flex layout (acts like position: absolute) */
|
|
23
|
+
flexExclude?: boolean;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export interface FlexContainerConfig {
|
|
@@ -123,6 +131,37 @@ function layoutLine(
|
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
// Shrink: if content overflows and mainSize is finite, shrink eligible items
|
|
135
|
+
if (totalGrow === 0 && mainSize > 0) {
|
|
136
|
+
const overflow = totalFixed + totalGap - mainSize;
|
|
137
|
+
if (overflow > 0) {
|
|
138
|
+
let totalShrinkable = 0;
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
141
|
+
if (shrink > 0) {
|
|
142
|
+
totalShrinkable += isRow ? item.w : item.h;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (totalShrinkable > 0) {
|
|
146
|
+
for (const item of items) {
|
|
147
|
+
const shrink = item.child._flexConfig?.flexShrink ?? 1;
|
|
148
|
+
if (shrink > 0) {
|
|
149
|
+
const itemMain = isRow ? item.w : item.h;
|
|
150
|
+
const reduction = overflow * (itemMain / totalShrinkable);
|
|
151
|
+
const newSize = Math.max(0, itemMain - reduction);
|
|
152
|
+
if (isRow) {
|
|
153
|
+
item.w = newSize;
|
|
154
|
+
item.child.width = newSize;
|
|
155
|
+
} else {
|
|
156
|
+
item.h = newSize;
|
|
157
|
+
item.child.height = newSize;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
126
165
|
// Calculate total main size after flex
|
|
127
166
|
let totalMain = totalGap;
|
|
128
167
|
for (const item of items) {
|
|
@@ -163,9 +202,12 @@ function layoutLine(
|
|
|
163
202
|
const mainDim = isRow ? item.w : item.h;
|
|
164
203
|
const crossDim = isRow ? item.h : item.w;
|
|
165
204
|
|
|
166
|
-
// Cross-axis alignment
|
|
205
|
+
// Cross-axis alignment (alignSelf overrides align)
|
|
206
|
+
const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
|
|
207
|
+
? item.child._flexConfig.alignSelf
|
|
208
|
+
: align;
|
|
167
209
|
let crossPos = crossOffset;
|
|
168
|
-
switch (
|
|
210
|
+
switch (effectiveAlign) {
|
|
169
211
|
case 'start':
|
|
170
212
|
break;
|
|
171
213
|
case 'center':
|
|
@@ -369,11 +411,13 @@ export class FlexContainer extends Container {
|
|
|
369
411
|
const mainLimit = isRow ? contentW : contentH;
|
|
370
412
|
const crossLimit = isRow ? contentH : contentW;
|
|
371
413
|
|
|
372
|
-
// Measure children
|
|
373
|
-
const measured: LineItem[] =
|
|
414
|
+
// Measure children (skip flexExclude — they position themselves)
|
|
415
|
+
const measured: LineItem[] = [];
|
|
416
|
+
for (const child of this._layoutChildren) {
|
|
417
|
+
if (child._flexConfig?.flexExclude) continue;
|
|
374
418
|
const { w, h, ox, oy } = measureChild(child);
|
|
375
|
-
|
|
376
|
-
}
|
|
419
|
+
measured.push({ child, w, h, ox, oy });
|
|
420
|
+
}
|
|
377
421
|
|
|
378
422
|
// Split into lines (if wrapping)
|
|
379
423
|
const lines: LineItem[][] = [];
|
|
@@ -419,6 +463,12 @@ export class FlexContainer extends Container {
|
|
|
419
463
|
// Offset items by padding
|
|
420
464
|
const tempItems = line.map((item) => ({ ...item }));
|
|
421
465
|
|
|
466
|
+
// For single-line layouts, use the full available cross space for alignment;
|
|
467
|
+
// for multi-line (wrapping), each line gets its own measured cross size.
|
|
468
|
+
const effectiveCross = lines.length === 1 && crossLimit < Infinity
|
|
469
|
+
? crossLimit
|
|
470
|
+
: (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
471
|
+
|
|
422
472
|
layoutLine(
|
|
423
473
|
tempItems,
|
|
424
474
|
isRow,
|
|
@@ -427,7 +477,7 @@ export class FlexContainer extends Container {
|
|
|
427
477
|
alignItems,
|
|
428
478
|
gap,
|
|
429
479
|
crossOffset,
|
|
430
|
-
|
|
480
|
+
effectiveCross,
|
|
431
481
|
);
|
|
432
482
|
|
|
433
483
|
// Apply main-axis padding offset
|
package/src/ui/Slider.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|
package/src/ui/Toggle.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Container, Graphics } from 'pixi.js';
|
|
2
|
+
import { Tween } from '../animation/Tween';
|
|
3
|
+
import { resolveView } from './view';
|
|
4
|
+
import type { ViewInput } from './view';
|
|
5
|
+
|
|
6
|
+
export interface ToggleConfig {
|
|
7
|
+
/** Initial state (default: false) */
|
|
8
|
+
value?: boolean;
|
|
9
|
+
/** Custom view for the ON state */
|
|
10
|
+
onView?: ViewInput;
|
|
11
|
+
/** Custom view for the OFF state */
|
|
12
|
+
offView?: ViewInput;
|
|
13
|
+
/** Width (for Graphics-based toggle, default: 52) */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** Height (for Graphics-based toggle, default: 28) */
|
|
16
|
+
height?: number;
|
|
17
|
+
/** Track color when ON (ignored when custom views provided) */
|
|
18
|
+
onColor?: number;
|
|
19
|
+
/** Track color when OFF (ignored when custom views provided) */
|
|
20
|
+
offColor?: number;
|
|
21
|
+
/** Handle color (for Graphics-based toggle) */
|
|
22
|
+
handleColor?: number;
|
|
23
|
+
/** Handle radius (for Graphics-based toggle, default: auto) */
|
|
24
|
+
handleRadius?: number;
|
|
25
|
+
/** Animation duration in ms (default: 200) */
|
|
26
|
+
animationDuration?: number;
|
|
27
|
+
|
|
28
|
+
/** Called when value changes */
|
|
29
|
+
onChange?: (value: boolean) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Toggle switch with two states.
|
|
34
|
+
*
|
|
35
|
+
* Supports custom ON/OFF views or auto-generated Graphics-based toggle.
|
|
36
|
+
* Click to toggle, or use `forceSwitch(value)` programmatically.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const mute = new Toggle({
|
|
41
|
+
* value: false,
|
|
42
|
+
* onColor: 0x22cc22,
|
|
43
|
+
* onChange: (on) => audioManager.mute(!on),
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class Toggle extends Container {
|
|
48
|
+
readonly __uiComponent = true as const;
|
|
49
|
+
|
|
50
|
+
private _value: boolean;
|
|
51
|
+
private _onView: Container | null = null;
|
|
52
|
+
private _offView: Container | null = null;
|
|
53
|
+
private _handle: Container | null = null;
|
|
54
|
+
private _trackGfx: Graphics | null = null;
|
|
55
|
+
private _config: Required<Pick<ToggleConfig, 'width' | 'height' | 'onColor' | 'offColor' | 'handleColor' | 'handleRadius' | 'animationDuration'>>;
|
|
56
|
+
private _useCustomViews: boolean;
|
|
57
|
+
|
|
58
|
+
onChange: ((value: boolean) => void) | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(config: ToggleConfig = {}) {
|
|
61
|
+
super();
|
|
62
|
+
|
|
63
|
+
this._config = {
|
|
64
|
+
width: config.width ?? 52,
|
|
65
|
+
height: config.height ?? 28,
|
|
66
|
+
onColor: config.onColor ?? 0x22cc22,
|
|
67
|
+
offColor: config.offColor ?? 0x666666,
|
|
68
|
+
handleColor: config.handleColor ?? 0xffffff,
|
|
69
|
+
handleRadius: config.handleRadius ?? 0, // 0 = auto
|
|
70
|
+
animationDuration: config.animationDuration ?? 200,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this._value = config.value ?? false;
|
|
74
|
+
this.onChange = config.onChange ?? null;
|
|
75
|
+
|
|
76
|
+
const customOn = resolveView(config.onView);
|
|
77
|
+
const customOff = resolveView(config.offView);
|
|
78
|
+
this._useCustomViews = !!(customOn || customOff);
|
|
79
|
+
|
|
80
|
+
if (this._useCustomViews) {
|
|
81
|
+
// Custom view mode: show/hide ON and OFF views
|
|
82
|
+
if (customOn) {
|
|
83
|
+
this._onView = customOn;
|
|
84
|
+
this._onView.visible = this._value;
|
|
85
|
+
this.addChild(this._onView);
|
|
86
|
+
}
|
|
87
|
+
if (customOff) {
|
|
88
|
+
this._offView = customOff;
|
|
89
|
+
this._offView.visible = !this._value;
|
|
90
|
+
this.addChild(this._offView);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// Graphics mode: track + sliding handle
|
|
94
|
+
const { width, height, handleColor } = this._config;
|
|
95
|
+
const handleRadius = this._config.handleRadius || (height / 2 - 3);
|
|
96
|
+
this._config.handleRadius = handleRadius;
|
|
97
|
+
|
|
98
|
+
this._trackGfx = new Graphics();
|
|
99
|
+
this.addChild(this._trackGfx);
|
|
100
|
+
this._drawTrack();
|
|
101
|
+
|
|
102
|
+
const handle = new Graphics();
|
|
103
|
+
handle.circle(0, 0, handleRadius).fill(handleColor);
|
|
104
|
+
handle.y = height / 2;
|
|
105
|
+
handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
|
|
106
|
+
this._handle = handle;
|
|
107
|
+
this.addChild(handle);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Interaction
|
|
111
|
+
this.eventMode = 'static';
|
|
112
|
+
this.cursor = 'pointer';
|
|
113
|
+
this.on('pointertap', this._onTap, this);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Current toggle state */
|
|
117
|
+
get value(): boolean {
|
|
118
|
+
return this._value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
set value(v: boolean) {
|
|
122
|
+
if (v === this._value) return;
|
|
123
|
+
this.forceSwitch(v);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Programmatically switch to a specific state with animation */
|
|
127
|
+
forceSwitch(value: boolean): void {
|
|
128
|
+
this._value = value;
|
|
129
|
+
this._animateToState();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** React reconciler update hook */
|
|
133
|
+
updateConfig(changed: Record<string, any>): void {
|
|
134
|
+
if ('value' in changed) this.value = changed.value;
|
|
135
|
+
if ('onChange' in changed) this.onChange = changed.onChange;
|
|
136
|
+
if ('animationDuration' in changed) this._config.animationDuration = changed.animationDuration;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private _onTap(): void {
|
|
140
|
+
this._value = !this._value;
|
|
141
|
+
this._animateToState();
|
|
142
|
+
this.onChange?.(this._value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _animateToState(): void {
|
|
146
|
+
const duration = this._config.animationDuration;
|
|
147
|
+
|
|
148
|
+
if (this._useCustomViews) {
|
|
149
|
+
// Custom views: crossfade
|
|
150
|
+
if (this._onView) {
|
|
151
|
+
Tween.killTweensOf(this._onView);
|
|
152
|
+
if (this._value) {
|
|
153
|
+
this._onView.visible = true;
|
|
154
|
+
Tween.to(this._onView, { alpha: 1 }, duration);
|
|
155
|
+
} else {
|
|
156
|
+
Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
|
|
157
|
+
if (this._onView) this._onView.visible = false;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (this._offView) {
|
|
162
|
+
Tween.killTweensOf(this._offView);
|
|
163
|
+
if (!this._value) {
|
|
164
|
+
this._offView.visible = true;
|
|
165
|
+
Tween.to(this._offView, { alpha: 1 }, duration);
|
|
166
|
+
} else {
|
|
167
|
+
Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
|
|
168
|
+
if (this._offView) this._offView.visible = false;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Graphics mode: slide handle + recolor track
|
|
174
|
+
this._drawTrack();
|
|
175
|
+
if (this._handle) {
|
|
176
|
+
const { width } = this._config;
|
|
177
|
+
const handleRadius = this._config.handleRadius;
|
|
178
|
+
const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
|
|
179
|
+
Tween.killTweensOf(this._handle);
|
|
180
|
+
Tween.to(this._handle, { x: targetX }, duration);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private _drawTrack(): void {
|
|
186
|
+
if (!this._trackGfx) return;
|
|
187
|
+
const { width, height, onColor, offColor } = this._config;
|
|
188
|
+
const radius = height / 2;
|
|
189
|
+
this._trackGfx.clear();
|
|
190
|
+
this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
|
|
194
|
+
this.off('pointertap', this._onTap, this);
|
|
195
|
+
if (this._handle) Tween.killTweensOf(this._handle);
|
|
196
|
+
if (this._onView) Tween.killTweensOf(this._onView);
|
|
197
|
+
if (this._offView) Tween.killTweensOf(this._offView);
|
|
198
|
+
this.onChange = null;
|
|
199
|
+
super.destroy(options);
|
|
200
|
+
}
|
|
201
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { ViewInput } from './view';
|
|
|
4
4
|
|
|
5
5
|
// ─── Engine UI Components ─────────────────────────────────
|
|
6
6
|
export { FlexContainer } from './FlexContainer';
|
|
7
|
-
export type { FlexContainerConfig, FlexDirection, JustifyContent, AlignItems, FlexItemConfig } from './FlexContainer';
|
|
7
|
+
export type { FlexContainerConfig, FlexDirection, JustifyContent, AlignItems, AlignSelf, FlexItemConfig } from './FlexContainer';
|
|
8
8
|
export { Button } from './Button';
|
|
9
9
|
export type { ButtonConfig, ButtonState } from './Button';
|
|
10
10
|
export { ProgressBar } from './ProgressBar';
|
|
@@ -25,3 +25,7 @@ export { Layout } from './Layout';
|
|
|
25
25
|
export type { LayoutConfig, LayoutDirection, LayoutAlignment, LayoutAnchor } from './Layout';
|
|
26
26
|
export { ScrollContainer } from './ScrollContainer';
|
|
27
27
|
export type { ScrollContainerConfig, ScrollDirection } from './ScrollContainer';
|
|
28
|
+
export { Slider } from './Slider';
|
|
29
|
+
export type { SliderConfig } from './Slider';
|
|
30
|
+
export { Toggle } from './Toggle';
|
|
31
|
+
export type { ToggleConfig } from './Toggle';
|