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