@energy8platform/game-engine 0.11.0 → 0.12.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/react.esm.js CHANGED
@@ -21,6 +21,8 @@ function extend(components) {
21
21
  }
22
22
 
23
23
  const RESERVED = new Set(['children', 'key', 'ref']);
24
+ /** Props handled by the reconciler as flex item config, not forwarded to components */
25
+ const FLEX_ITEM_PROPS$1 = new Set(['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude']);
24
26
  // ─── UI Component helpers ────────────────────────────────
25
27
  /**
26
28
  * Extract a config object from React props.
@@ -30,7 +32,7 @@ const RESERVED = new Set(['children', 'key', 'ref']);
30
32
  function extractConfig(props) {
31
33
  const config = {};
32
34
  for (const key in props) {
33
- if (RESERVED.has(key) || isEventProp(key))
35
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || isEventProp(key))
34
36
  continue;
35
37
  if (key.includes('-')) {
36
38
  const parts = key.split('-');
@@ -55,7 +57,7 @@ function diffConfig(newProps, oldProps) {
55
57
  const changed = {};
56
58
  // New or changed props
57
59
  for (const key in newProps) {
58
- if (RESERVED.has(key) || isEventProp(key))
60
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || isEventProp(key))
59
61
  continue;
60
62
  if (newProps[key] !== oldProps[key]) {
61
63
  if (key.includes('-')) {
@@ -154,7 +156,7 @@ function setNestedValue(target, path, value) {
154
156
  function applyProps(instance, newProps, oldProps = {}) {
155
157
  // Remove old props not in newProps
156
158
  for (const key in oldProps) {
157
- if (RESERVED.has(key) || key in newProps)
159
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key) || key in newProps)
158
160
  continue;
159
161
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
160
162
  if (pixiEvent) {
@@ -173,7 +175,7 @@ function applyProps(instance, newProps, oldProps = {}) {
173
175
  }
174
176
  // Apply new props
175
177
  for (const key in newProps) {
176
- if (RESERVED.has(key))
178
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS$1.has(key))
177
179
  continue;
178
180
  const value = newProps[key];
179
181
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
@@ -199,182 +201,6 @@ function applyProps(instance, newProps, oldProps = {}) {
199
201
  }
200
202
  }
201
203
 
202
- function toPascalCase(str) {
203
- return str.charAt(0).toUpperCase() + str.slice(1);
204
- }
205
- const hostConfig = {
206
- isPrimaryRenderer: false,
207
- supportsMutation: true,
208
- supportsPersistence: false,
209
- supportsHydration: false,
210
- createInstance(type, props) {
211
- const name = toPascalCase(type);
212
- const Ctor = catalogue[name];
213
- if (!Ctor) {
214
- throw new Error(`[PixiReconciler] Unknown element "<${type}>". ` +
215
- `Call extend({ ${name} }) before rendering.`);
216
- }
217
- let instance;
218
- if (Ctor.prototype.__uiComponent) {
219
- // Config-based UI component: pass props as constructor config
220
- const config = extractConfig(props);
221
- instance = new Ctor(config);
222
- applyEventProps(instance, props);
223
- }
224
- else {
225
- // Standard PixiJS element
226
- instance = new Ctor();
227
- applyProps(instance, props);
228
- }
229
- if (hasEventProps(props) && instance.eventMode === 'auto') {
230
- instance.eventMode = 'static';
231
- }
232
- return instance;
233
- },
234
- createTextInstance() {
235
- throw new Error('[PixiReconciler] Text strings are not supported. Use a <text> element.');
236
- },
237
- appendInitialChild(parent, child) {
238
- if (child instanceof Container)
239
- parent.addChild(child);
240
- },
241
- appendChild(parent, child) {
242
- if (child instanceof Container)
243
- parent.addChild(child);
244
- },
245
- appendChildToContainer(container, child) {
246
- if (child instanceof Container)
247
- container.addChild(child);
248
- },
249
- removeChild(parent, child) {
250
- if (child instanceof Container) {
251
- parent.removeChild(child);
252
- child.destroy({ children: true });
253
- }
254
- },
255
- removeChildFromContainer(container, child) {
256
- if (child instanceof Container) {
257
- container.removeChild(child);
258
- child.destroy({ children: true });
259
- }
260
- },
261
- insertBefore(parent, child, beforeChild) {
262
- if (child instanceof Container && beforeChild instanceof Container) {
263
- if (child.parent)
264
- child.parent.removeChild(child);
265
- const index = parent.getChildIndex(beforeChild);
266
- parent.addChildAt(child, index);
267
- }
268
- },
269
- insertInContainerBefore(container, child, beforeChild) {
270
- if (child instanceof Container && beforeChild instanceof Container) {
271
- if (child.parent)
272
- child.parent.removeChild(child);
273
- const index = container.getChildIndex(beforeChild);
274
- container.addChildAt(child, index);
275
- }
276
- },
277
- commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
278
- if (instance.__uiComponent && typeof instance.updateConfig === 'function') {
279
- const changed = diffConfig(newProps, oldProps);
280
- if (Object.keys(changed).length > 0) {
281
- instance.updateConfig(changed);
282
- }
283
- applyEventProps(instance, newProps, oldProps);
284
- }
285
- else {
286
- applyProps(instance, newProps, oldProps);
287
- }
288
- if (hasEventProps(newProps) && instance.eventMode === 'auto') {
289
- instance.eventMode = 'static';
290
- }
291
- },
292
- finalizeInitialChildren() {
293
- return false;
294
- },
295
- prepareUpdate() {
296
- return true;
297
- },
298
- shouldSetTextContent() {
299
- return false;
300
- },
301
- getRootHostContext() {
302
- return null;
303
- },
304
- getChildHostContext(parentHostContext) {
305
- return parentHostContext;
306
- },
307
- getPublicInstance(instance) {
308
- return instance;
309
- },
310
- prepareForCommit() {
311
- return null;
312
- },
313
- resetAfterCommit() { },
314
- preparePortalMount() { },
315
- scheduleTimeout: setTimeout,
316
- cancelTimeout: clearTimeout,
317
- noTimeout: -1,
318
- getCurrentEventPriority() {
319
- return DefaultEventPriority;
320
- },
321
- hideInstance(instance) {
322
- instance.visible = false;
323
- },
324
- unhideInstance(instance) {
325
- instance.visible = true;
326
- },
327
- hideTextInstance() { },
328
- unhideTextInstance() { },
329
- clearContainer() { },
330
- detachDeletedInstance() { },
331
- prepareScopeUpdate() { },
332
- getInstanceFromNode() { return null; },
333
- getInstanceFromScope() { return null; },
334
- beforeActiveInstanceBlur() { },
335
- afterActiveInstanceBlur() { },
336
- };
337
- const reconciler = Reconciler(hostConfig);
338
-
339
- function createPixiRoot(container) {
340
- const fiberRoot = reconciler.createContainer(container, // containerInfo
341
- ConcurrentRoot, // tag
342
- null, // hydrationCallbacks
343
- false, // isStrictMode
344
- null, // concurrentUpdatesByDefaultOverride
345
- '', // identifierPrefix
346
- (err) => console.error('[PixiRoot]', err), null);
347
- return {
348
- render(element) {
349
- reconciler.updateContainer(element, fiberRoot, null, () => { });
350
- },
351
- unmount() {
352
- reconciler.updateContainer(null, fiberRoot, null, () => { });
353
- },
354
- };
355
- }
356
-
357
- /**
358
- * Resolve a ViewInput to a Container instance.
359
- *
360
- * @example
361
- * ```ts
362
- * resolveView('btn-idle') // → Sprite.from('btn-idle')
363
- * resolveView(someTexture) // → new Sprite(someTexture)
364
- * resolveView(myCustomContainer) // → myCustomContainer (as-is)
365
- * resolveView(undefined) // → null
366
- * ```
367
- */
368
- function resolveView(input) {
369
- if (input == null)
370
- return null;
371
- if (typeof input === 'string')
372
- return Sprite.from(input);
373
- if (input instanceof Texture)
374
- return new Sprite(input);
375
- return input;
376
- }
377
-
378
204
  // ─── Helpers ─────────────────────────────────────────────
379
205
  function normalizePadding(p) {
380
206
  return typeof p === 'number' ? [p, p, p, p] : p;
@@ -433,6 +259,37 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
433
259
  }
434
260
  }
435
261
  }
262
+ // Shrink: if content overflows and mainSize is finite, shrink eligible items
263
+ if (totalGrow === 0 && mainSize > 0) {
264
+ const overflow = totalFixed + totalGap - mainSize;
265
+ if (overflow > 0) {
266
+ let totalShrinkable = 0;
267
+ for (const item of items) {
268
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
269
+ if (shrink > 0) {
270
+ totalShrinkable += isRow ? item.w : item.h;
271
+ }
272
+ }
273
+ if (totalShrinkable > 0) {
274
+ for (const item of items) {
275
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
276
+ if (shrink > 0) {
277
+ const itemMain = isRow ? item.w : item.h;
278
+ const reduction = overflow * (itemMain / totalShrinkable);
279
+ const newSize = Math.max(0, itemMain - reduction);
280
+ if (isRow) {
281
+ item.w = newSize;
282
+ item.child.width = newSize;
283
+ }
284
+ else {
285
+ item.h = newSize;
286
+ item.child.height = newSize;
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
436
293
  // Calculate total main size after flex
437
294
  let totalMain = totalGap;
438
295
  for (const item of items) {
@@ -469,9 +326,12 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
469
326
  for (const item of items) {
470
327
  const mainDim = isRow ? item.w : item.h;
471
328
  const crossDim = isRow ? item.h : item.w;
472
- // Cross-axis alignment
329
+ // Cross-axis alignment (alignSelf overrides align)
330
+ const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
331
+ ? item.child._flexConfig.alignSelf
332
+ : align;
473
333
  let crossPos = crossOffset;
474
- switch (align) {
334
+ switch (effectiveAlign) {
475
335
  case 'start':
476
336
  break;
477
337
  case 'center':
@@ -655,11 +515,14 @@ class FlexContainer extends Container {
655
515
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
656
516
  const mainLimit = isRow ? contentW : contentH;
657
517
  const crossLimit = isRow ? contentH : contentW;
658
- // Measure children
659
- const measured = this._layoutChildren.map((child) => {
518
+ // Measure children (skip flexExclude — they position themselves)
519
+ const measured = [];
520
+ for (const child of this._layoutChildren) {
521
+ if (child._flexConfig?.flexExclude)
522
+ continue;
660
523
  const { w, h, ox, oy } = measureChild(child);
661
- return { child, w, h, ox, oy };
662
- });
524
+ measured.push({ child, w, h, ox, oy });
525
+ }
663
526
  // Split into lines (if wrapping)
664
527
  const lines = [];
665
528
  if (flexWrap && mainLimit < Infinity) {
@@ -702,7 +565,12 @@ class FlexContainer extends Container {
702
565
  const mainStart = isRow ? pl : pt;
703
566
  // Offset items by padding
704
567
  const tempItems = line.map((item) => ({ ...item }));
705
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
568
+ // For single-line layouts, use the full available cross space for alignment;
569
+ // for multi-line (wrapping), each line gets its own measured cross size.
570
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity
571
+ ? crossLimit
572
+ : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
573
+ layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
706
574
  // Apply main-axis padding offset
707
575
  for (const item of tempItems) {
708
576
  const origChild = line.find((l) => l.child === item.child);
@@ -723,36 +591,263 @@ class FlexContainer extends Container {
723
591
  maxX = Math.max(maxX, child.x + w);
724
592
  maxY = Math.max(maxY, child.y + h);
725
593
  }
726
- const [, pr, pb] = this._padding;
727
- return { width: maxX + pr, height: maxY + pb };
728
- }
729
- /** React reconciler update hook — applies changed config props */
730
- updateConfig(changed) {
731
- if ('direction' in changed)
732
- this.setDirection(changed.direction);
733
- if ('justifyContent' in changed)
734
- this.setJustifyContent(changed.justifyContent);
735
- if ('alignItems' in changed)
736
- this.setAlignItems(changed.alignItems);
737
- if ('gap' in changed)
738
- this.setGap(changed.gap);
739
- if ('padding' in changed)
740
- this.setPadding(changed.padding);
741
- if ('flexWrap' in changed) {
742
- this._config.flexWrap = changed.flexWrap;
743
- this._layoutDirty = true;
594
+ const [, pr, pb] = this._padding;
595
+ return { width: maxX + pr, height: maxY + pb };
596
+ }
597
+ /** React reconciler update hook — applies changed config props */
598
+ updateConfig(changed) {
599
+ if ('direction' in changed)
600
+ this.setDirection(changed.direction);
601
+ if ('justifyContent' in changed)
602
+ this.setJustifyContent(changed.justifyContent);
603
+ if ('alignItems' in changed)
604
+ this.setAlignItems(changed.alignItems);
605
+ if ('gap' in changed)
606
+ this.setGap(changed.gap);
607
+ if ('padding' in changed)
608
+ this.setPadding(changed.padding);
609
+ if ('flexWrap' in changed) {
610
+ this._config.flexWrap = changed.flexWrap;
611
+ this._layoutDirty = true;
612
+ }
613
+ if ('width' in changed || 'height' in changed) {
614
+ this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
615
+ return; // resize calls updateLayout
616
+ }
617
+ if (this._layoutDirty)
618
+ this.updateLayout();
619
+ }
620
+ destroy(options) {
621
+ this._layoutChildren.length = 0;
622
+ super.destroy(options);
623
+ }
624
+ }
625
+
626
+ /** Flex item prop names that should be forwarded to _flexConfig on the child */
627
+ const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude'];
628
+ /** Extract FlexItemConfig from props if any flex item props are present */
629
+ function extractFlexItemConfig(props) {
630
+ let config;
631
+ for (const key of FLEX_ITEM_PROPS) {
632
+ if (key in props) {
633
+ if (!config)
634
+ config = {};
635
+ config[key] = props[key];
636
+ }
637
+ }
638
+ return config;
639
+ }
640
+ /** Apply flex item config to a child being added to a FlexContainer */
641
+ function addChildToFlex(parent, child) {
642
+ const flexConfig = child._flexConfig;
643
+ if (flexConfig && Object.keys(flexConfig).length > 0) {
644
+ parent.addFlexChild(child, flexConfig);
645
+ }
646
+ else {
647
+ parent.addChild(child);
648
+ }
649
+ }
650
+ function toPascalCase(str) {
651
+ return str.charAt(0).toUpperCase() + str.slice(1);
652
+ }
653
+ const hostConfig = {
654
+ isPrimaryRenderer: false,
655
+ supportsMutation: true,
656
+ supportsPersistence: false,
657
+ supportsHydration: false,
658
+ createInstance(type, props) {
659
+ const name = toPascalCase(type);
660
+ const Ctor = catalogue[name];
661
+ if (!Ctor) {
662
+ throw new Error(`[PixiReconciler] Unknown element "<${type}>". ` +
663
+ `Call extend({ ${name} }) before rendering.`);
664
+ }
665
+ let instance;
666
+ if (Ctor.prototype.__uiComponent) {
667
+ // Config-based UI component: pass props as constructor config
668
+ const config = extractConfig(props);
669
+ instance = new Ctor(config);
670
+ applyEventProps(instance, props);
671
+ }
672
+ else {
673
+ // Standard PixiJS element
674
+ instance = new Ctor();
675
+ applyProps(instance, props);
676
+ }
677
+ if (hasEventProps(props) && instance.eventMode === 'auto') {
678
+ instance.eventMode = 'static';
679
+ }
680
+ // Store flex item config for when this child is added to a FlexContainer parent
681
+ const flexItemConfig = extractFlexItemConfig(props);
682
+ if (flexItemConfig) {
683
+ instance._flexConfig = { ...instance._flexConfig, ...flexItemConfig };
684
+ }
685
+ return instance;
686
+ },
687
+ createTextInstance() {
688
+ throw new Error('[PixiReconciler] Text strings are not supported. Use a <text> element.');
689
+ },
690
+ appendInitialChild(parent, child) {
691
+ if (child instanceof Container) {
692
+ if (parent instanceof FlexContainer) {
693
+ addChildToFlex(parent, child);
694
+ }
695
+ else {
696
+ parent.addChild(child);
697
+ }
698
+ }
699
+ },
700
+ appendChild(parent, child) {
701
+ if (child instanceof Container) {
702
+ if (parent instanceof FlexContainer) {
703
+ addChildToFlex(parent, child);
704
+ }
705
+ else {
706
+ parent.addChild(child);
707
+ }
708
+ }
709
+ },
710
+ appendChildToContainer(container, child) {
711
+ if (child instanceof Container)
712
+ container.addChild(child);
713
+ },
714
+ removeChild(parent, child) {
715
+ if (child instanceof Container) {
716
+ parent.removeChild(child);
717
+ child.destroy({ children: true });
718
+ }
719
+ },
720
+ removeChildFromContainer(container, child) {
721
+ if (child instanceof Container) {
722
+ container.removeChild(child);
723
+ child.destroy({ children: true });
724
+ }
725
+ },
726
+ insertBefore(parent, child, beforeChild) {
727
+ if (child instanceof Container && beforeChild instanceof Container) {
728
+ if (child.parent)
729
+ child.parent.removeChild(child);
730
+ const index = parent.getChildIndex(beforeChild);
731
+ parent.addChildAt(child, index);
732
+ }
733
+ },
734
+ insertInContainerBefore(container, child, beforeChild) {
735
+ if (child instanceof Container && beforeChild instanceof Container) {
736
+ if (child.parent)
737
+ child.parent.removeChild(child);
738
+ const index = container.getChildIndex(beforeChild);
739
+ container.addChildAt(child, index);
740
+ }
741
+ },
742
+ commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
743
+ if (instance.__uiComponent && typeof instance.updateConfig === 'function') {
744
+ const changed = diffConfig(newProps, oldProps);
745
+ if (Object.keys(changed).length > 0) {
746
+ instance.updateConfig(changed);
747
+ }
748
+ applyEventProps(instance, newProps, oldProps);
749
+ }
750
+ else {
751
+ applyProps(instance, newProps, oldProps);
752
+ }
753
+ // Update flex item config if parent is FlexContainer
754
+ const newFlexConfig = extractFlexItemConfig(newProps);
755
+ const oldFlexConfig = extractFlexItemConfig(oldProps);
756
+ if (newFlexConfig || oldFlexConfig) {
757
+ instance._flexConfig = { ...instance._flexConfig, ...newFlexConfig };
758
+ // Trigger parent relayout
759
+ if (instance.parent instanceof FlexContainer) {
760
+ instance.parent.updateLayout();
761
+ }
744
762
  }
745
- if ('width' in changed || 'height' in changed) {
746
- this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
747
- return; // resize calls updateLayout
763
+ if (hasEventProps(newProps) && instance.eventMode === 'auto') {
764
+ instance.eventMode = 'static';
748
765
  }
749
- if (this._layoutDirty)
750
- this.updateLayout();
751
- }
752
- destroy(options) {
753
- this._layoutChildren.length = 0;
754
- super.destroy(options);
755
- }
766
+ },
767
+ finalizeInitialChildren() {
768
+ return false;
769
+ },
770
+ prepareUpdate() {
771
+ return true;
772
+ },
773
+ shouldSetTextContent() {
774
+ return false;
775
+ },
776
+ getRootHostContext() {
777
+ return null;
778
+ },
779
+ getChildHostContext(parentHostContext) {
780
+ return parentHostContext;
781
+ },
782
+ getPublicInstance(instance) {
783
+ return instance;
784
+ },
785
+ prepareForCommit() {
786
+ return null;
787
+ },
788
+ resetAfterCommit() { },
789
+ preparePortalMount() { },
790
+ scheduleTimeout: setTimeout,
791
+ cancelTimeout: clearTimeout,
792
+ noTimeout: -1,
793
+ getCurrentEventPriority() {
794
+ return DefaultEventPriority;
795
+ },
796
+ hideInstance(instance) {
797
+ instance.visible = false;
798
+ },
799
+ unhideInstance(instance) {
800
+ instance.visible = true;
801
+ },
802
+ hideTextInstance() { },
803
+ unhideTextInstance() { },
804
+ clearContainer() { },
805
+ detachDeletedInstance() { },
806
+ prepareScopeUpdate() { },
807
+ getInstanceFromNode() { return null; },
808
+ getInstanceFromScope() { return null; },
809
+ beforeActiveInstanceBlur() { },
810
+ afterActiveInstanceBlur() { },
811
+ };
812
+ const reconciler = Reconciler(hostConfig);
813
+
814
+ function createPixiRoot(container) {
815
+ const fiberRoot = reconciler.createContainer(container, // containerInfo
816
+ ConcurrentRoot, // tag
817
+ null, // hydrationCallbacks
818
+ false, // isStrictMode
819
+ null, // concurrentUpdatesByDefaultOverride
820
+ '', // identifierPrefix
821
+ (err) => console.error('[PixiRoot]', err), null);
822
+ return {
823
+ render(element) {
824
+ reconciler.updateContainer(element, fiberRoot, null, () => { });
825
+ },
826
+ unmount() {
827
+ reconciler.updateContainer(null, fiberRoot, null, () => { });
828
+ },
829
+ };
830
+ }
831
+
832
+ /**
833
+ * Resolve a ViewInput to a Container instance.
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * resolveView('btn-idle') // → Sprite.from('btn-idle')
838
+ * resolveView(someTexture) // → new Sprite(someTexture)
839
+ * resolveView(myCustomContainer) // → myCustomContainer (as-is)
840
+ * resolveView(undefined) // → null
841
+ * ```
842
+ */
843
+ function resolveView(input) {
844
+ if (input == null)
845
+ return null;
846
+ if (typeof input === 'string')
847
+ return Sprite.from(input);
848
+ if (input instanceof Texture)
849
+ return new Sprite(input);
850
+ return input;
756
851
  }
757
852
 
758
853
  /**
@@ -2604,6 +2699,362 @@ class ScrollContainer extends Container {
2604
2699
  }
2605
2700
  }
2606
2701
 
2702
+ /**
2703
+ * Draggable slider with customizable track, fill, and handle views.
2704
+ *
2705
+ * @example
2706
+ * ```ts
2707
+ * const volume = new Slider({
2708
+ * min: 0, max: 1, value: 0.5,
2709
+ * width: 200, height: 8,
2710
+ * fillColor: 0xffd700,
2711
+ * onUpdate: (v) => console.log('Volume:', v),
2712
+ * });
2713
+ * ```
2714
+ */
2715
+ class Slider extends Container {
2716
+ __uiComponent = true;
2717
+ _track;
2718
+ _fill;
2719
+ _fillMask;
2720
+ _handle;
2721
+ _config;
2722
+ _value;
2723
+ _dragging = false;
2724
+ onUpdate = null;
2725
+ onChange = null;
2726
+ constructor(config = {}) {
2727
+ super();
2728
+ this._config = {
2729
+ min: config.min ?? 0,
2730
+ max: config.max ?? 1,
2731
+ step: config.step ?? 0,
2732
+ width: config.width ?? 200,
2733
+ height: config.height ?? 8,
2734
+ borderRadius: config.borderRadius ?? 4,
2735
+ trackColor: config.trackColor ?? 0x333333,
2736
+ fillColor: config.fillColor ?? 0xffd700,
2737
+ handleRadius: config.handleRadius ?? 12,
2738
+ handleColor: config.handleColor ?? 0xffffff,
2739
+ };
2740
+ this._value = config.value ?? this._config.min;
2741
+ this.onUpdate = config.onUpdate ?? null;
2742
+ this.onChange = config.onChange ?? null;
2743
+ const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
2744
+ // Track
2745
+ const customTrack = resolveView(config.trackView);
2746
+ if (customTrack) {
2747
+ customTrack.width = width;
2748
+ customTrack.height = height;
2749
+ this._track = customTrack;
2750
+ }
2751
+ else {
2752
+ const g = new Graphics();
2753
+ g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
2754
+ this._track = g;
2755
+ }
2756
+ this.addChild(this._track);
2757
+ // Fill
2758
+ const customFill = resolveView(config.fillView);
2759
+ if (customFill) {
2760
+ customFill.width = width;
2761
+ customFill.height = height;
2762
+ this._fill = customFill;
2763
+ }
2764
+ else {
2765
+ const g = new Graphics();
2766
+ g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
2767
+ this._fill = g;
2768
+ }
2769
+ this.addChild(this._fill);
2770
+ // Fill mask
2771
+ this._fillMask = new Graphics();
2772
+ this.addChild(this._fillMask);
2773
+ this._fill.mask = this._fillMask;
2774
+ // Handle
2775
+ const customHandle = resolveView(config.handleView);
2776
+ if (customHandle) {
2777
+ this._handle = customHandle;
2778
+ }
2779
+ else {
2780
+ const g = new Graphics();
2781
+ g.circle(0, 0, handleRadius).fill(handleColor);
2782
+ this._handle = g;
2783
+ }
2784
+ this._handle.y = height / 2;
2785
+ this.addChild(this._handle);
2786
+ // Interaction
2787
+ this.eventMode = 'static';
2788
+ this.cursor = 'pointer';
2789
+ // Hit area covers track + handle overflow
2790
+ const hitPad = Math.max(handleRadius - height / 2, 0);
2791
+ this.hitArea = { contains: (x, y) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
2792
+ this.on('pointerdown', this._onPointerDown, this);
2793
+ this.on('globalpointermove', this._onPointerMove, this);
2794
+ this.on('pointerup', this._onPointerUp, this);
2795
+ this.on('pointerupoutside', this._onPointerUp, this);
2796
+ this._updateVisuals();
2797
+ }
2798
+ /** Current value */
2799
+ get value() {
2800
+ return this._value;
2801
+ }
2802
+ set value(v) {
2803
+ const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
2804
+ if (clamped === this._value)
2805
+ return;
2806
+ this._value = clamped;
2807
+ this._updateVisuals();
2808
+ }
2809
+ get min() { return this._config.min; }
2810
+ get max() { return this._config.max; }
2811
+ /** React reconciler update hook */
2812
+ updateConfig(changed) {
2813
+ if ('value' in changed)
2814
+ this.value = changed.value;
2815
+ if ('min' in changed) {
2816
+ this._config.min = changed.min;
2817
+ this._updateVisuals();
2818
+ }
2819
+ if ('max' in changed) {
2820
+ this._config.max = changed.max;
2821
+ this._updateVisuals();
2822
+ }
2823
+ if ('step' in changed)
2824
+ this._config.step = changed.step;
2825
+ if ('onUpdate' in changed)
2826
+ this.onUpdate = changed.onUpdate;
2827
+ if ('onChange' in changed)
2828
+ this.onChange = changed.onChange;
2829
+ }
2830
+ _fraction() {
2831
+ const { min, max } = this._config;
2832
+ return max === min ? 0 : (this._value - min) / (max - min);
2833
+ }
2834
+ _applyStep(v) {
2835
+ const { step, min } = this._config;
2836
+ if (step <= 0)
2837
+ return v;
2838
+ return min + Math.round((v - min) / step) * step;
2839
+ }
2840
+ _updateVisuals() {
2841
+ const frac = this._fraction();
2842
+ const w = this._config.width;
2843
+ const h = this._config.height;
2844
+ // Update fill mask
2845
+ this._fillMask.clear();
2846
+ this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
2847
+ // Update handle position
2848
+ this._handle.x = w * frac;
2849
+ }
2850
+ _valueFromPointer(e) {
2851
+ const local = this.toLocal(e.global);
2852
+ const frac = Math.max(0, Math.min(1, local.x / this._config.width));
2853
+ const { min, max } = this._config;
2854
+ return this._applyStep(min + frac * (max - min));
2855
+ }
2856
+ _onPointerDown(e) {
2857
+ this._dragging = true;
2858
+ const newValue = this._valueFromPointer(e);
2859
+ if (newValue !== this._value) {
2860
+ this._value = newValue;
2861
+ this._updateVisuals();
2862
+ this.onUpdate?.(this._value);
2863
+ }
2864
+ }
2865
+ _onPointerMove(e) {
2866
+ if (!this._dragging)
2867
+ return;
2868
+ const newValue = this._valueFromPointer(e);
2869
+ if (newValue !== this._value) {
2870
+ this._value = newValue;
2871
+ this._updateVisuals();
2872
+ this.onUpdate?.(this._value);
2873
+ }
2874
+ }
2875
+ _onPointerUp(_e) {
2876
+ if (!this._dragging)
2877
+ return;
2878
+ this._dragging = false;
2879
+ this.onChange?.(this._value);
2880
+ }
2881
+ destroy(options) {
2882
+ this.off('pointerdown', this._onPointerDown, this);
2883
+ this.off('globalpointermove', this._onPointerMove, this);
2884
+ this.off('pointerup', this._onPointerUp, this);
2885
+ this.off('pointerupoutside', this._onPointerUp, this);
2886
+ this.onUpdate = null;
2887
+ this.onChange = null;
2888
+ super.destroy(options);
2889
+ }
2890
+ }
2891
+
2892
+ /**
2893
+ * Toggle switch with two states.
2894
+ *
2895
+ * Supports custom ON/OFF views or auto-generated Graphics-based toggle.
2896
+ * Click to toggle, or use `forceSwitch(value)` programmatically.
2897
+ *
2898
+ * @example
2899
+ * ```ts
2900
+ * const mute = new Toggle({
2901
+ * value: false,
2902
+ * onColor: 0x22cc22,
2903
+ * onChange: (on) => audioManager.mute(!on),
2904
+ * });
2905
+ * ```
2906
+ */
2907
+ class Toggle extends Container {
2908
+ __uiComponent = true;
2909
+ _value;
2910
+ _onView = null;
2911
+ _offView = null;
2912
+ _handle = null;
2913
+ _trackGfx = null;
2914
+ _config;
2915
+ _useCustomViews;
2916
+ onChange = null;
2917
+ constructor(config = {}) {
2918
+ super();
2919
+ this._config = {
2920
+ width: config.width ?? 52,
2921
+ height: config.height ?? 28,
2922
+ onColor: config.onColor ?? 0x22cc22,
2923
+ offColor: config.offColor ?? 0x666666,
2924
+ handleColor: config.handleColor ?? 0xffffff,
2925
+ handleRadius: config.handleRadius ?? 0, // 0 = auto
2926
+ animationDuration: config.animationDuration ?? 200,
2927
+ };
2928
+ this._value = config.value ?? false;
2929
+ this.onChange = config.onChange ?? null;
2930
+ const customOn = resolveView(config.onView);
2931
+ const customOff = resolveView(config.offView);
2932
+ this._useCustomViews = !!(customOn || customOff);
2933
+ if (this._useCustomViews) {
2934
+ // Custom view mode: show/hide ON and OFF views
2935
+ if (customOn) {
2936
+ this._onView = customOn;
2937
+ this._onView.visible = this._value;
2938
+ this.addChild(this._onView);
2939
+ }
2940
+ if (customOff) {
2941
+ this._offView = customOff;
2942
+ this._offView.visible = !this._value;
2943
+ this.addChild(this._offView);
2944
+ }
2945
+ }
2946
+ else {
2947
+ // Graphics mode: track + sliding handle
2948
+ const { width, height, handleColor } = this._config;
2949
+ const handleRadius = this._config.handleRadius || (height / 2 - 3);
2950
+ this._config.handleRadius = handleRadius;
2951
+ this._trackGfx = new Graphics();
2952
+ this.addChild(this._trackGfx);
2953
+ this._drawTrack();
2954
+ const handle = new Graphics();
2955
+ handle.circle(0, 0, handleRadius).fill(handleColor);
2956
+ handle.y = height / 2;
2957
+ handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
2958
+ this._handle = handle;
2959
+ this.addChild(handle);
2960
+ }
2961
+ // Interaction
2962
+ this.eventMode = 'static';
2963
+ this.cursor = 'pointer';
2964
+ this.on('pointertap', this._onTap, this);
2965
+ }
2966
+ /** Current toggle state */
2967
+ get value() {
2968
+ return this._value;
2969
+ }
2970
+ set value(v) {
2971
+ if (v === this._value)
2972
+ return;
2973
+ this.forceSwitch(v);
2974
+ }
2975
+ /** Programmatically switch to a specific state with animation */
2976
+ forceSwitch(value) {
2977
+ this._value = value;
2978
+ this._animateToState();
2979
+ }
2980
+ /** React reconciler update hook */
2981
+ updateConfig(changed) {
2982
+ if ('value' in changed)
2983
+ this.value = changed.value;
2984
+ if ('onChange' in changed)
2985
+ this.onChange = changed.onChange;
2986
+ if ('animationDuration' in changed)
2987
+ this._config.animationDuration = changed.animationDuration;
2988
+ }
2989
+ _onTap() {
2990
+ this._value = !this._value;
2991
+ this._animateToState();
2992
+ this.onChange?.(this._value);
2993
+ }
2994
+ _animateToState() {
2995
+ const duration = this._config.animationDuration;
2996
+ if (this._useCustomViews) {
2997
+ // Custom views: crossfade
2998
+ if (this._onView) {
2999
+ Tween.killTweensOf(this._onView);
3000
+ if (this._value) {
3001
+ this._onView.visible = true;
3002
+ Tween.to(this._onView, { alpha: 1 }, duration);
3003
+ }
3004
+ else {
3005
+ Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
3006
+ if (this._onView)
3007
+ this._onView.visible = false;
3008
+ });
3009
+ }
3010
+ }
3011
+ if (this._offView) {
3012
+ Tween.killTweensOf(this._offView);
3013
+ if (!this._value) {
3014
+ this._offView.visible = true;
3015
+ Tween.to(this._offView, { alpha: 1 }, duration);
3016
+ }
3017
+ else {
3018
+ Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
3019
+ if (this._offView)
3020
+ this._offView.visible = false;
3021
+ });
3022
+ }
3023
+ }
3024
+ }
3025
+ else {
3026
+ // Graphics mode: slide handle + recolor track
3027
+ this._drawTrack();
3028
+ if (this._handle) {
3029
+ const { width } = this._config;
3030
+ const handleRadius = this._config.handleRadius;
3031
+ const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
3032
+ Tween.killTweensOf(this._handle);
3033
+ Tween.to(this._handle, { x: targetX }, duration);
3034
+ }
3035
+ }
3036
+ }
3037
+ _drawTrack() {
3038
+ if (!this._trackGfx)
3039
+ return;
3040
+ const { width, height, onColor, offColor } = this._config;
3041
+ const radius = height / 2;
3042
+ this._trackGfx.clear();
3043
+ this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
3044
+ }
3045
+ destroy(options) {
3046
+ this.off('pointertap', this._onTap, this);
3047
+ if (this._handle)
3048
+ Tween.killTweensOf(this._handle);
3049
+ if (this._onView)
3050
+ Tween.killTweensOf(this._onView);
3051
+ if (this._offView)
3052
+ Tween.killTweensOf(this._offView);
3053
+ this.onChange = null;
3054
+ super.destroy(options);
3055
+ }
3056
+ }
3057
+
2607
3058
  /**
2608
3059
  * Register all standard PixiJS display objects for JSX use.
2609
3060
  * Call once at app startup before rendering any React scenes.
@@ -2644,6 +3095,7 @@ function extendUIElements() {
2644
3095
  extend({
2645
3096
  Button, Label, Panel, FlexContainer, ProgressBar,
2646
3097
  ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
3098
+ Slider, Toggle,
2647
3099
  });
2648
3100
  }
2649
3101
  /**