@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/ui.cjs.js CHANGED
@@ -81,6 +81,37 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
81
81
  }
82
82
  }
83
83
  }
84
+ // Shrink: if content overflows and mainSize is finite, shrink eligible items
85
+ if (totalGrow === 0 && mainSize > 0) {
86
+ const overflow = totalFixed + totalGap - mainSize;
87
+ if (overflow > 0) {
88
+ let totalShrinkable = 0;
89
+ for (const item of items) {
90
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
91
+ if (shrink > 0) {
92
+ totalShrinkable += isRow ? item.w : item.h;
93
+ }
94
+ }
95
+ if (totalShrinkable > 0) {
96
+ for (const item of items) {
97
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
98
+ if (shrink > 0) {
99
+ const itemMain = isRow ? item.w : item.h;
100
+ const reduction = overflow * (itemMain / totalShrinkable);
101
+ const newSize = Math.max(0, itemMain - reduction);
102
+ if (isRow) {
103
+ item.w = newSize;
104
+ item.child.width = newSize;
105
+ }
106
+ else {
107
+ item.h = newSize;
108
+ item.child.height = newSize;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
84
115
  // Calculate total main size after flex
85
116
  let totalMain = totalGap;
86
117
  for (const item of items) {
@@ -117,9 +148,12 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
117
148
  for (const item of items) {
118
149
  const mainDim = isRow ? item.w : item.h;
119
150
  const crossDim = isRow ? item.h : item.w;
120
- // Cross-axis alignment
151
+ // Cross-axis alignment (alignSelf overrides align)
152
+ const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
153
+ ? item.child._flexConfig.alignSelf
154
+ : align;
121
155
  let crossPos = crossOffset;
122
- switch (align) {
156
+ switch (effectiveAlign) {
123
157
  case 'start':
124
158
  break;
125
159
  case 'center':
@@ -303,11 +337,14 @@ class FlexContainer extends pixi_js.Container {
303
337
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
304
338
  const mainLimit = isRow ? contentW : contentH;
305
339
  const crossLimit = isRow ? contentH : contentW;
306
- // Measure children
307
- const measured = this._layoutChildren.map((child) => {
340
+ // Measure children (skip flexExclude — they position themselves)
341
+ const measured = [];
342
+ for (const child of this._layoutChildren) {
343
+ if (child._flexConfig?.flexExclude)
344
+ continue;
308
345
  const { w, h, ox, oy } = measureChild(child);
309
- return { child, w, h, ox, oy };
310
- });
346
+ measured.push({ child, w, h, ox, oy });
347
+ }
311
348
  // Split into lines (if wrapping)
312
349
  const lines = [];
313
350
  if (flexWrap && mainLimit < Infinity) {
@@ -350,7 +387,12 @@ class FlexContainer extends pixi_js.Container {
350
387
  const mainStart = isRow ? pl : pt;
351
388
  // Offset items by padding
352
389
  const tempItems = line.map((item) => ({ ...item }));
353
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
390
+ // For single-line layouts, use the full available cross space for alignment;
391
+ // for multi-line (wrapping), each line gets its own measured cross size.
392
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity
393
+ ? crossLimit
394
+ : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
395
+ layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
354
396
  // Apply main-axis padding offset
355
397
  for (const item of tempItems) {
356
398
  const origChild = line.find((l) => l.child === item.child);
@@ -2252,6 +2294,362 @@ class ScrollContainer extends pixi_js.Container {
2252
2294
  }
2253
2295
  }
2254
2296
 
2297
+ /**
2298
+ * Draggable slider with customizable track, fill, and handle views.
2299
+ *
2300
+ * @example
2301
+ * ```ts
2302
+ * const volume = new Slider({
2303
+ * min: 0, max: 1, value: 0.5,
2304
+ * width: 200, height: 8,
2305
+ * fillColor: 0xffd700,
2306
+ * onUpdate: (v) => console.log('Volume:', v),
2307
+ * });
2308
+ * ```
2309
+ */
2310
+ class Slider extends pixi_js.Container {
2311
+ __uiComponent = true;
2312
+ _track;
2313
+ _fill;
2314
+ _fillMask;
2315
+ _handle;
2316
+ _config;
2317
+ _value;
2318
+ _dragging = false;
2319
+ onUpdate = null;
2320
+ onChange = null;
2321
+ constructor(config = {}) {
2322
+ super();
2323
+ this._config = {
2324
+ min: config.min ?? 0,
2325
+ max: config.max ?? 1,
2326
+ step: config.step ?? 0,
2327
+ width: config.width ?? 200,
2328
+ height: config.height ?? 8,
2329
+ borderRadius: config.borderRadius ?? 4,
2330
+ trackColor: config.trackColor ?? 0x333333,
2331
+ fillColor: config.fillColor ?? 0xffd700,
2332
+ handleRadius: config.handleRadius ?? 12,
2333
+ handleColor: config.handleColor ?? 0xffffff,
2334
+ };
2335
+ this._value = config.value ?? this._config.min;
2336
+ this.onUpdate = config.onUpdate ?? null;
2337
+ this.onChange = config.onChange ?? null;
2338
+ const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
2339
+ // Track
2340
+ const customTrack = resolveView(config.trackView);
2341
+ if (customTrack) {
2342
+ customTrack.width = width;
2343
+ customTrack.height = height;
2344
+ this._track = customTrack;
2345
+ }
2346
+ else {
2347
+ const g = new pixi_js.Graphics();
2348
+ g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
2349
+ this._track = g;
2350
+ }
2351
+ this.addChild(this._track);
2352
+ // Fill
2353
+ const customFill = resolveView(config.fillView);
2354
+ if (customFill) {
2355
+ customFill.width = width;
2356
+ customFill.height = height;
2357
+ this._fill = customFill;
2358
+ }
2359
+ else {
2360
+ const g = new pixi_js.Graphics();
2361
+ g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
2362
+ this._fill = g;
2363
+ }
2364
+ this.addChild(this._fill);
2365
+ // Fill mask
2366
+ this._fillMask = new pixi_js.Graphics();
2367
+ this.addChild(this._fillMask);
2368
+ this._fill.mask = this._fillMask;
2369
+ // Handle
2370
+ const customHandle = resolveView(config.handleView);
2371
+ if (customHandle) {
2372
+ this._handle = customHandle;
2373
+ }
2374
+ else {
2375
+ const g = new pixi_js.Graphics();
2376
+ g.circle(0, 0, handleRadius).fill(handleColor);
2377
+ this._handle = g;
2378
+ }
2379
+ this._handle.y = height / 2;
2380
+ this.addChild(this._handle);
2381
+ // Interaction
2382
+ this.eventMode = 'static';
2383
+ this.cursor = 'pointer';
2384
+ // Hit area covers track + handle overflow
2385
+ const hitPad = Math.max(handleRadius - height / 2, 0);
2386
+ this.hitArea = { contains: (x, y) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
2387
+ this.on('pointerdown', this._onPointerDown, this);
2388
+ this.on('globalpointermove', this._onPointerMove, this);
2389
+ this.on('pointerup', this._onPointerUp, this);
2390
+ this.on('pointerupoutside', this._onPointerUp, this);
2391
+ this._updateVisuals();
2392
+ }
2393
+ /** Current value */
2394
+ get value() {
2395
+ return this._value;
2396
+ }
2397
+ set value(v) {
2398
+ const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
2399
+ if (clamped === this._value)
2400
+ return;
2401
+ this._value = clamped;
2402
+ this._updateVisuals();
2403
+ }
2404
+ get min() { return this._config.min; }
2405
+ get max() { return this._config.max; }
2406
+ /** React reconciler update hook */
2407
+ updateConfig(changed) {
2408
+ if ('value' in changed)
2409
+ this.value = changed.value;
2410
+ if ('min' in changed) {
2411
+ this._config.min = changed.min;
2412
+ this._updateVisuals();
2413
+ }
2414
+ if ('max' in changed) {
2415
+ this._config.max = changed.max;
2416
+ this._updateVisuals();
2417
+ }
2418
+ if ('step' in changed)
2419
+ this._config.step = changed.step;
2420
+ if ('onUpdate' in changed)
2421
+ this.onUpdate = changed.onUpdate;
2422
+ if ('onChange' in changed)
2423
+ this.onChange = changed.onChange;
2424
+ }
2425
+ _fraction() {
2426
+ const { min, max } = this._config;
2427
+ return max === min ? 0 : (this._value - min) / (max - min);
2428
+ }
2429
+ _applyStep(v) {
2430
+ const { step, min } = this._config;
2431
+ if (step <= 0)
2432
+ return v;
2433
+ return min + Math.round((v - min) / step) * step;
2434
+ }
2435
+ _updateVisuals() {
2436
+ const frac = this._fraction();
2437
+ const w = this._config.width;
2438
+ const h = this._config.height;
2439
+ // Update fill mask
2440
+ this._fillMask.clear();
2441
+ this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
2442
+ // Update handle position
2443
+ this._handle.x = w * frac;
2444
+ }
2445
+ _valueFromPointer(e) {
2446
+ const local = this.toLocal(e.global);
2447
+ const frac = Math.max(0, Math.min(1, local.x / this._config.width));
2448
+ const { min, max } = this._config;
2449
+ return this._applyStep(min + frac * (max - min));
2450
+ }
2451
+ _onPointerDown(e) {
2452
+ this._dragging = true;
2453
+ const newValue = this._valueFromPointer(e);
2454
+ if (newValue !== this._value) {
2455
+ this._value = newValue;
2456
+ this._updateVisuals();
2457
+ this.onUpdate?.(this._value);
2458
+ }
2459
+ }
2460
+ _onPointerMove(e) {
2461
+ if (!this._dragging)
2462
+ return;
2463
+ const newValue = this._valueFromPointer(e);
2464
+ if (newValue !== this._value) {
2465
+ this._value = newValue;
2466
+ this._updateVisuals();
2467
+ this.onUpdate?.(this._value);
2468
+ }
2469
+ }
2470
+ _onPointerUp(_e) {
2471
+ if (!this._dragging)
2472
+ return;
2473
+ this._dragging = false;
2474
+ this.onChange?.(this._value);
2475
+ }
2476
+ destroy(options) {
2477
+ this.off('pointerdown', this._onPointerDown, this);
2478
+ this.off('globalpointermove', this._onPointerMove, this);
2479
+ this.off('pointerup', this._onPointerUp, this);
2480
+ this.off('pointerupoutside', this._onPointerUp, this);
2481
+ this.onUpdate = null;
2482
+ this.onChange = null;
2483
+ super.destroy(options);
2484
+ }
2485
+ }
2486
+
2487
+ /**
2488
+ * Toggle switch with two states.
2489
+ *
2490
+ * Supports custom ON/OFF views or auto-generated Graphics-based toggle.
2491
+ * Click to toggle, or use `forceSwitch(value)` programmatically.
2492
+ *
2493
+ * @example
2494
+ * ```ts
2495
+ * const mute = new Toggle({
2496
+ * value: false,
2497
+ * onColor: 0x22cc22,
2498
+ * onChange: (on) => audioManager.mute(!on),
2499
+ * });
2500
+ * ```
2501
+ */
2502
+ class Toggle extends pixi_js.Container {
2503
+ __uiComponent = true;
2504
+ _value;
2505
+ _onView = null;
2506
+ _offView = null;
2507
+ _handle = null;
2508
+ _trackGfx = null;
2509
+ _config;
2510
+ _useCustomViews;
2511
+ onChange = null;
2512
+ constructor(config = {}) {
2513
+ super();
2514
+ this._config = {
2515
+ width: config.width ?? 52,
2516
+ height: config.height ?? 28,
2517
+ onColor: config.onColor ?? 0x22cc22,
2518
+ offColor: config.offColor ?? 0x666666,
2519
+ handleColor: config.handleColor ?? 0xffffff,
2520
+ handleRadius: config.handleRadius ?? 0, // 0 = auto
2521
+ animationDuration: config.animationDuration ?? 200,
2522
+ };
2523
+ this._value = config.value ?? false;
2524
+ this.onChange = config.onChange ?? null;
2525
+ const customOn = resolveView(config.onView);
2526
+ const customOff = resolveView(config.offView);
2527
+ this._useCustomViews = !!(customOn || customOff);
2528
+ if (this._useCustomViews) {
2529
+ // Custom view mode: show/hide ON and OFF views
2530
+ if (customOn) {
2531
+ this._onView = customOn;
2532
+ this._onView.visible = this._value;
2533
+ this.addChild(this._onView);
2534
+ }
2535
+ if (customOff) {
2536
+ this._offView = customOff;
2537
+ this._offView.visible = !this._value;
2538
+ this.addChild(this._offView);
2539
+ }
2540
+ }
2541
+ else {
2542
+ // Graphics mode: track + sliding handle
2543
+ const { width, height, handleColor } = this._config;
2544
+ const handleRadius = this._config.handleRadius || (height / 2 - 3);
2545
+ this._config.handleRadius = handleRadius;
2546
+ this._trackGfx = new pixi_js.Graphics();
2547
+ this.addChild(this._trackGfx);
2548
+ this._drawTrack();
2549
+ const handle = new pixi_js.Graphics();
2550
+ handle.circle(0, 0, handleRadius).fill(handleColor);
2551
+ handle.y = height / 2;
2552
+ handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
2553
+ this._handle = handle;
2554
+ this.addChild(handle);
2555
+ }
2556
+ // Interaction
2557
+ this.eventMode = 'static';
2558
+ this.cursor = 'pointer';
2559
+ this.on('pointertap', this._onTap, this);
2560
+ }
2561
+ /** Current toggle state */
2562
+ get value() {
2563
+ return this._value;
2564
+ }
2565
+ set value(v) {
2566
+ if (v === this._value)
2567
+ return;
2568
+ this.forceSwitch(v);
2569
+ }
2570
+ /** Programmatically switch to a specific state with animation */
2571
+ forceSwitch(value) {
2572
+ this._value = value;
2573
+ this._animateToState();
2574
+ }
2575
+ /** React reconciler update hook */
2576
+ updateConfig(changed) {
2577
+ if ('value' in changed)
2578
+ this.value = changed.value;
2579
+ if ('onChange' in changed)
2580
+ this.onChange = changed.onChange;
2581
+ if ('animationDuration' in changed)
2582
+ this._config.animationDuration = changed.animationDuration;
2583
+ }
2584
+ _onTap() {
2585
+ this._value = !this._value;
2586
+ this._animateToState();
2587
+ this.onChange?.(this._value);
2588
+ }
2589
+ _animateToState() {
2590
+ const duration = this._config.animationDuration;
2591
+ if (this._useCustomViews) {
2592
+ // Custom views: crossfade
2593
+ if (this._onView) {
2594
+ Tween.killTweensOf(this._onView);
2595
+ if (this._value) {
2596
+ this._onView.visible = true;
2597
+ Tween.to(this._onView, { alpha: 1 }, duration);
2598
+ }
2599
+ else {
2600
+ Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
2601
+ if (this._onView)
2602
+ this._onView.visible = false;
2603
+ });
2604
+ }
2605
+ }
2606
+ if (this._offView) {
2607
+ Tween.killTweensOf(this._offView);
2608
+ if (!this._value) {
2609
+ this._offView.visible = true;
2610
+ Tween.to(this._offView, { alpha: 1 }, duration);
2611
+ }
2612
+ else {
2613
+ Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
2614
+ if (this._offView)
2615
+ this._offView.visible = false;
2616
+ });
2617
+ }
2618
+ }
2619
+ }
2620
+ else {
2621
+ // Graphics mode: slide handle + recolor track
2622
+ this._drawTrack();
2623
+ if (this._handle) {
2624
+ const { width } = this._config;
2625
+ const handleRadius = this._config.handleRadius;
2626
+ const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
2627
+ Tween.killTweensOf(this._handle);
2628
+ Tween.to(this._handle, { x: targetX }, duration);
2629
+ }
2630
+ }
2631
+ }
2632
+ _drawTrack() {
2633
+ if (!this._trackGfx)
2634
+ return;
2635
+ const { width, height, onColor, offColor } = this._config;
2636
+ const radius = height / 2;
2637
+ this._trackGfx.clear();
2638
+ this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
2639
+ }
2640
+ destroy(options) {
2641
+ this.off('pointertap', this._onTap, this);
2642
+ if (this._handle)
2643
+ Tween.killTweensOf(this._handle);
2644
+ if (this._onView)
2645
+ Tween.killTweensOf(this._onView);
2646
+ if (this._offView)
2647
+ Tween.killTweensOf(this._offView);
2648
+ this.onChange = null;
2649
+ super.destroy(options);
2650
+ }
2651
+ }
2652
+
2255
2653
  exports.BalanceDisplay = BalanceDisplay;
2256
2654
  exports.Button = Button;
2257
2655
  exports.FlexContainer = FlexContainer;
@@ -2261,7 +2659,9 @@ exports.Modal = Modal;
2261
2659
  exports.Panel = Panel;
2262
2660
  exports.ProgressBar = ProgressBar;
2263
2661
  exports.ScrollContainer = ScrollContainer;
2662
+ exports.Slider = Slider;
2264
2663
  exports.Toast = Toast;
2664
+ exports.Toggle = Toggle;
2265
2665
  exports.WinDisplay = WinDisplay;
2266
2666
  exports.resolveView = resolveView;
2267
2667
  //# sourceMappingURL=ui.cjs.js.map