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