@energy8platform/game-engine 0.3.0 → 0.5.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/src/ui/Button.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { Container, Graphics, Sprite, Texture } from 'pixi.js';
2
- import { Tween } from '../animation/Tween';
3
- import { Easing } from '../animation/Easing';
1
+ import { Graphics, Texture } from 'pixi.js';
2
+ import { FancyButton } from '@pixi/ui';
3
+ import type { ButtonOptions } from '@pixi/ui';
4
4
 
5
- export type ButtonState = 'normal' | 'hover' | 'pressed' | 'disabled';
5
+ export type ButtonState = 'default' | 'hover' | 'pressed' | 'disabled';
6
6
 
7
7
  export interface ButtonConfig {
8
8
  /** Default texture/sprite for each state (optional — uses Graphics if not provided) */
@@ -21,179 +21,120 @@ export interface ButtonConfig {
21
21
  animationDuration?: number;
22
22
  /** Start disabled */
23
23
  disabled?: boolean;
24
+ /** Button text */
25
+ text?: string;
26
+ /** Button text style */
27
+ textStyle?: Record<string, unknown>;
24
28
  }
25
29
 
26
30
  const DEFAULT_COLORS: Record<ButtonState, number> = {
27
- normal: 0xffd700,
31
+ default: 0xffd700,
28
32
  hover: 0xffe44d,
29
33
  pressed: 0xccac00,
30
34
  disabled: 0x666666,
31
35
  };
32
36
 
37
+ function makeGraphicsView(
38
+ w: number, h: number, radius: number, color: number,
39
+ ): Graphics {
40
+ const g = new Graphics();
41
+ g.roundRect(0, 0, w, h, radius).fill(color);
42
+ // Highlight overlay
43
+ g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
44
+ return g;
45
+ }
46
+
33
47
  /**
34
- * Interactive button component with state management and animation.
48
+ * Interactive button component powered by `@pixi/ui` FancyButton.
35
49
  *
36
- * Supports both texture-based and Graphics-based rendering.
50
+ * Supports both texture-based and Graphics-based rendering with
51
+ * per-state views, press animation, and text.
37
52
  *
38
53
  * @example
39
54
  * ```ts
40
55
  * const btn = new Button({
41
56
  * width: 200, height: 60, borderRadius: 12,
42
- * colors: { normal: 0x22aa22, hover: 0x33cc33 },
57
+ * colors: { default: 0x22aa22, hover: 0x33cc33 },
58
+ * text: 'SPIN',
43
59
  * });
44
60
  *
45
- * btn.onTap = () => console.log('Clicked!');
61
+ * btn.onPress.connect(() => console.log('Clicked!'));
46
62
  * scene.container.addChild(btn);
47
63
  * ```
48
64
  */
49
- export class Button extends Container {
50
- private _state: ButtonState = 'normal';
51
- private _bg: Graphics;
52
- private _sprites: Partial<Record<ButtonState, Sprite>> = {};
53
- private _config: Required<
65
+ export class Button extends FancyButton {
66
+ private _buttonConfig: Required<
54
67
  Pick<ButtonConfig, 'width' | 'height' | 'borderRadius' | 'pressScale' | 'animationDuration'>
55
68
  > & ButtonConfig;
56
69
 
57
- /** Called when the button is tapped/clicked */
58
- public onTap?: () => void;
59
-
60
- /** Called when the button state changes */
61
- public onStateChange?: (state: ButtonState) => void;
62
-
63
70
  constructor(config: ButtonConfig = {}) {
64
- super();
65
-
66
- this._config = {
67
- width: 200,
68
- height: 60,
69
- borderRadius: 8,
70
- pressScale: 0.95,
71
- animationDuration: 100,
71
+ const resolvedConfig = {
72
+ width: config.width ?? 200,
73
+ height: config.height ?? 60,
74
+ borderRadius: config.borderRadius ?? 8,
75
+ pressScale: config.pressScale ?? 0.95,
76
+ animationDuration: config.animationDuration ?? 100,
72
77
  ...config,
73
78
  };
74
79
 
75
- // Create Graphics background
76
- this._bg = new Graphics();
77
- this.addChild(this._bg);
80
+ const colorMap = { ...DEFAULT_COLORS, ...config.colors };
81
+ const { width, height, borderRadius } = resolvedConfig;
82
+
83
+ // Build FancyButton options
84
+ const options: ButtonOptions = {
85
+ anchor: 0.5,
86
+ animations: {
87
+ hover: {
88
+ props: { scale: { x: 1.03, y: 1.03 } },
89
+ duration: resolvedConfig.animationDuration,
90
+ },
91
+ pressed: {
92
+ props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
93
+ duration: resolvedConfig.animationDuration,
94
+ },
95
+ },
96
+ };
78
97
 
79
- // Create texture sprites if provided
98
+ // Texture-based views
80
99
  if (config.textures) {
81
- for (const [state, tex] of Object.entries(config.textures)) {
82
- const texture = typeof tex === 'string' ? Texture.from(tex) : tex;
83
- const sprite = new Sprite(texture);
84
- sprite.anchor.set(0.5);
85
- sprite.visible = state === 'normal';
86
- this._sprites[state as ButtonState] = sprite;
87
- this.addChild(sprite);
88
- }
100
+ if (config.textures.default) options.defaultView = config.textures.default as any;
101
+ if (config.textures.hover) options.hoverView = config.textures.hover as any;
102
+ if (config.textures.pressed) options.pressedView = config.textures.pressed as any;
103
+ if (config.textures.disabled) options.disabledView = config.textures.disabled as any;
104
+ } else {
105
+ // Graphics-based views
106
+ options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
107
+ options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
108
+ options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
109
+ options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
89
110
  }
90
111
 
91
- // Make interactive
92
- this.eventMode = 'static';
93
- this.cursor = 'pointer';
94
-
95
- // Set up hit area for Graphics-based
96
- this.pivot.set(this._config.width / 2, this._config.height / 2);
112
+ // Text
113
+ if (config.text) {
114
+ options.text = config.text;
115
+ }
97
116
 
98
- // Bind events
99
- this.on('pointerover', this.onPointerOver);
100
- this.on('pointerout', this.onPointerOut);
101
- this.on('pointerdown', this.onPointerDown);
102
- this.on('pointerup', this.onPointerUp);
103
- this.on('pointertap', this.onPointerTap);
117
+ super(options);
104
118
 
105
- // Initial render
106
- this.setState('normal');
119
+ this._buttonConfig = resolvedConfig;
107
120
 
108
121
  if (config.disabled) {
109
- this.disable();
122
+ this.enabled = false;
110
123
  }
111
124
  }
112
125
 
113
- /** Current button state */
114
- get state(): ButtonState {
115
- return this._state;
116
- }
117
-
118
126
  /** Enable the button */
119
127
  enable(): void {
120
- if (this._state === 'disabled') {
121
- this.setState('normal');
122
- this.eventMode = 'static';
123
- this.cursor = 'pointer';
124
- }
128
+ this.enabled = true;
125
129
  }
126
130
 
127
131
  /** Disable the button */
128
132
  disable(): void {
129
- this.setState('disabled');
130
- this.eventMode = 'none';
131
- this.cursor = 'default';
133
+ this.enabled = false;
132
134
  }
133
135
 
134
136
  /** Whether the button is disabled */
135
137
  get disabled(): boolean {
136
- return this._state === 'disabled';
138
+ return !this.enabled;
137
139
  }
138
-
139
- private setState(state: ButtonState): void {
140
- if (this._state === state) return;
141
- this._state = state;
142
- this.render();
143
- this.onStateChange?.(state);
144
- }
145
-
146
- private render(): void {
147
- const { width, height, borderRadius, colors } = this._config;
148
- const colorMap = { ...DEFAULT_COLORS, ...colors };
149
-
150
- // Update Graphics
151
- this._bg.clear();
152
- this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
153
-
154
- // Add highlight for normal/hover
155
- if (this._state === 'normal' || this._state === 'hover') {
156
- this._bg
157
- .roundRect(2, 2, width - 4, height * 0.45, borderRadius)
158
- .fill({ color: 0xffffff, alpha: 0.1 });
159
- }
160
-
161
- // Update sprite visibility
162
- for (const [state, sprite] of Object.entries(this._sprites)) {
163
- if (sprite) sprite.visible = state === this._state;
164
- }
165
- // Fall back to normal sprite if state sprite doesn't exist
166
- if (!this._sprites[this._state] && this._sprites.normal) {
167
- this._sprites.normal.visible = true;
168
- }
169
- }
170
-
171
- private onPointerOver = (): void => {
172
- if (this._state === 'disabled') return;
173
- this.setState('hover');
174
- };
175
-
176
- private onPointerOut = (): void => {
177
- if (this._state === 'disabled') return;
178
- this.setState('normal');
179
- Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
180
- };
181
-
182
- private onPointerDown = (): void => {
183
- if (this._state === 'disabled') return;
184
- this.setState('pressed');
185
- const s = this._config.pressScale;
186
- Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
187
- };
188
-
189
- private onPointerUp = (): void => {
190
- if (this._state === 'disabled') return;
191
- this.setState('hover');
192
- Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
193
- };
194
-
195
- private onPointerTap = (): void => {
196
- if (this._state === 'disabled') return;
197
- this.onTap?.();
198
- };
199
140
  }
package/src/ui/Layout.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Container } from 'pixi.js';
2
+ import type { LayoutStyles } from '@pixi/layout';
2
3
 
3
4
  // ─── Types ───────────────────────────────────────────────
4
5
 
@@ -38,8 +39,65 @@ export interface LayoutConfig {
38
39
  breakpoints?: Record<number, Partial<LayoutConfig>>;
39
40
  }
40
41
 
42
+ // ─── Helpers ─────────────────────────────────────────────
43
+
44
+ const ALIGNMENT_MAP: Record<LayoutAlignment, LayoutStyles['alignItems']> = {
45
+ start: 'flex-start',
46
+ center: 'center',
47
+ end: 'flex-end',
48
+ stretch: 'stretch',
49
+ };
50
+
51
+ function normalizePadding(
52
+ padding: number | [number, number, number, number],
53
+ ): [number, number, number, number] {
54
+ if (typeof padding === 'number') return [padding, padding, padding, padding];
55
+ return padding;
56
+ }
57
+
58
+ function directionToFlexStyles(
59
+ direction: LayoutDirection,
60
+ maxWidth: number,
61
+ ): Partial<LayoutStyles> {
62
+ switch (direction) {
63
+ case 'horizontal':
64
+ return { flexDirection: 'row', flexWrap: 'nowrap' };
65
+ case 'vertical':
66
+ return { flexDirection: 'column', flexWrap: 'nowrap' };
67
+ case 'grid':
68
+ return { flexDirection: 'row', flexWrap: 'wrap' };
69
+ case 'wrap':
70
+ return {
71
+ flexDirection: 'row',
72
+ flexWrap: 'wrap',
73
+ ...(maxWidth < Infinity ? { maxWidth } : {}),
74
+ };
75
+ }
76
+ }
77
+
78
+ function buildLayoutStyles(config: {
79
+ direction: LayoutDirection;
80
+ gap: number;
81
+ alignment: LayoutAlignment;
82
+ columns: number;
83
+ padding: [number, number, number, number];
84
+ maxWidth: number;
85
+ }): LayoutStyles {
86
+ const [pt, pr, pb, pl] = config.padding;
87
+
88
+ return {
89
+ ...directionToFlexStyles(config.direction, config.maxWidth),
90
+ gap: config.gap,
91
+ alignItems: ALIGNMENT_MAP[config.alignment],
92
+ paddingTop: pt,
93
+ paddingRight: pr,
94
+ paddingBottom: pb,
95
+ paddingLeft: pl,
96
+ };
97
+ }
98
+
41
99
  /**
42
- * Responsive layout container that automatically arranges its children.
100
+ * Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
43
101
  *
44
102
  * Supports horizontal, vertical, grid, and wrap layout modes with
45
103
  * alignment, padding, gap, and viewport-anchor positioning.
@@ -62,17 +120,15 @@ export interface LayoutConfig {
62
120
  * toolbar.addItem(betLabel);
63
121
  * scene.container.addChild(toolbar);
64
122
  *
65
- * // On resize, update layout position relative to viewport
66
123
  * toolbar.updateViewport(width, height);
67
124
  * ```
68
125
  */
69
126
  export class Layout extends Container {
70
- private _config: Required<Pick<LayoutConfig, 'direction' | 'gap' | 'alignment' | 'autoLayout' | 'columns'>>;
127
+ private _layoutConfig: Required<Pick<LayoutConfig, 'direction' | 'gap' | 'alignment' | 'autoLayout' | 'columns'>>;
71
128
  private _padding: [number, number, number, number];
72
129
  private _anchor: LayoutAnchor;
73
130
  private _maxWidth: number;
74
131
  private _breakpoints: [number, Partial<LayoutConfig>][];
75
- private _content: Container;
76
132
  private _items: Container[] = [];
77
133
  private _viewportWidth = 0;
78
134
  private _viewportHeight = 0;
@@ -80,7 +136,7 @@ export class Layout extends Container {
80
136
  constructor(config: LayoutConfig = {}) {
81
137
  super();
82
138
 
83
- this._config = {
139
+ this._layoutConfig = {
84
140
  direction: config.direction ?? 'vertical',
85
141
  gap: config.gap ?? 0,
86
142
  alignment: config.alignment ?? 'start',
@@ -88,26 +144,28 @@ export class Layout extends Container {
88
144
  columns: config.columns ?? 2,
89
145
  };
90
146
 
91
- this._padding = Layout.normalizePadding(config.padding ?? 0);
147
+ this._padding = normalizePadding(config.padding ?? 0);
92
148
  this._anchor = config.anchor ?? 'top-left';
93
149
  this._maxWidth = config.maxWidth ?? Infinity;
94
150
 
95
- // Sort breakpoints by width ascending for correct resolution
96
151
  this._breakpoints = config.breakpoints
97
152
  ? Object.entries(config.breakpoints)
98
153
  .map(([w, cfg]) => [Number(w), cfg] as [number, Partial<LayoutConfig>])
99
154
  .sort((a, b) => a[0] - b[0])
100
155
  : [];
101
156
 
102
- this._content = new Container();
103
- this.addChild(this._content);
157
+ this.applyLayoutStyles();
104
158
  }
105
159
 
106
160
  /** Add an item to the layout */
107
161
  addItem(child: Container): this {
108
162
  this._items.push(child);
109
- this._content.addChild(child);
110
- if (this._config.autoLayout) this.layout();
163
+ this.addChild(child);
164
+
165
+ if (this._layoutConfig.direction === 'grid') {
166
+ this.applyGridChildWidth(child);
167
+ }
168
+
111
169
  return this;
112
170
  }
113
171
 
@@ -116,8 +174,7 @@ export class Layout extends Container {
116
174
  const idx = this._items.indexOf(child);
117
175
  if (idx !== -1) {
118
176
  this._items.splice(idx, 1);
119
- this._content.removeChild(child);
120
- if (this._config.autoLayout) this.layout();
177
+ this.removeChild(child);
121
178
  }
122
179
  return this;
123
180
  }
@@ -125,10 +182,9 @@ export class Layout extends Container {
125
182
  /** Remove all items */
126
183
  clearItems(): this {
127
184
  for (const item of this._items) {
128
- this._content.removeChild(item);
185
+ this.removeChild(item);
129
186
  }
130
187
  this._items.length = 0;
131
- if (this._config.autoLayout) this.layout();
132
188
  return this;
133
189
  }
134
190
 
@@ -144,192 +200,81 @@ export class Layout extends Container {
144
200
  updateViewport(width: number, height: number): void {
145
201
  this._viewportWidth = width;
146
202
  this._viewportHeight = height;
147
- this.layout();
203
+ this.applyLayoutStyles();
204
+ this.applyAnchor();
148
205
  }
149
206
 
150
- /**
151
- * Recalculate layout positions of all children.
152
- */
153
- layout(): void {
154
- if (this._items.length === 0) return;
155
-
156
- // Resolve effective config (apply breakpoint overrides)
207
+ private applyLayoutStyles(): void {
157
208
  const effective = this.resolveConfig();
158
- const gap = effective.gap ?? this._config.gap;
159
- const direction = effective.direction ?? this._config.direction;
160
- const alignment = effective.alignment ?? this._config.alignment;
161
- const columns = effective.columns ?? this._config.columns;
209
+ const direction = effective.direction ?? this._layoutConfig.direction;
210
+ const gap = effective.gap ?? this._layoutConfig.gap;
211
+ const alignment = effective.alignment ?? this._layoutConfig.alignment;
212
+ const columns = effective.columns ?? this._layoutConfig.columns;
162
213
  const padding = effective.padding !== undefined
163
- ? Layout.normalizePadding(effective.padding)
214
+ ? normalizePadding(effective.padding)
164
215
  : this._padding;
165
216
  const maxWidth = effective.maxWidth ?? this._maxWidth;
166
217
 
167
- const [pt, pr, pb, pl] = padding;
168
-
169
- switch (direction) {
170
- case 'horizontal':
171
- this.layoutLinear('x', 'y', gap, alignment, pl, pt);
172
- break;
173
- case 'vertical':
174
- this.layoutLinear('y', 'x', gap, alignment, pt, pl);
175
- break;
176
- case 'grid':
177
- this.layoutGrid(columns, gap, alignment, pl, pt);
178
- break;
179
- case 'wrap':
180
- this.layoutWrap(maxWidth - pl - pr, gap, alignment, pl, pt);
181
- break;
182
- }
183
-
184
- // Apply anchor positioning relative to viewport
185
- this.applyAnchor(effective.anchor ?? this._anchor);
186
- }
218
+ const styles = buildLayoutStyles({ direction, gap, alignment, columns, padding, maxWidth });
219
+ this.layout = styles;
187
220
 
188
- // ─── Private layout helpers ────────────────────────────
189
-
190
- private layoutLinear(
191
- mainAxis: 'x' | 'y',
192
- crossAxis: 'x' | 'y',
193
- gap: number,
194
- alignment: LayoutAlignment,
195
- mainOffset: number,
196
- crossOffset: number,
197
- ): void {
198
- let pos = mainOffset;
199
- const sizes = this._items.map(item => this.getItemSize(item));
200
- const maxCross = Math.max(...sizes.map(s => (crossAxis === 'x' ? s.width : s.height)));
201
-
202
- for (let i = 0; i < this._items.length; i++) {
203
- const item = this._items[i];
204
- const size = sizes[i];
205
-
206
- item[mainAxis] = pos;
207
-
208
- // Cross-axis alignment
209
- const itemCross = crossAxis === 'x' ? size.width : size.height;
210
- switch (alignment) {
211
- case 'start':
212
- item[crossAxis] = crossOffset;
213
- break;
214
- case 'center':
215
- item[crossAxis] = crossOffset + (maxCross - itemCross) / 2;
216
- break;
217
- case 'end':
218
- item[crossAxis] = crossOffset + maxCross - itemCross;
219
- break;
220
- case 'stretch':
221
- item[crossAxis] = crossOffset;
222
- // Note: stretch doesn't resize children — that's up to the item
223
- break;
221
+ if (direction === 'grid') {
222
+ for (const item of this._items) {
223
+ this.applyGridChildWidth(item);
224
224
  }
225
-
226
- const mainSize = mainAxis === 'x' ? size.width : size.height;
227
- pos += mainSize + gap;
228
225
  }
229
226
  }
230
227
 
231
- private layoutGrid(
232
- columns: number,
233
- gap: number,
234
- alignment: LayoutAlignment,
235
- offsetX: number,
236
- offsetY: number,
237
- ): void {
238
- const sizes = this._items.map(item => this.getItemSize(item));
239
- const maxItemWidth = Math.max(...sizes.map(s => s.width));
240
- const maxItemHeight = Math.max(...sizes.map(s => s.height));
241
- const cellW = maxItemWidth + gap;
242
- const cellH = maxItemHeight + gap;
243
-
244
- for (let i = 0; i < this._items.length; i++) {
245
- const item = this._items[i];
246
- const col = i % columns;
247
- const row = Math.floor(i / columns);
248
- const size = sizes[i];
249
-
250
- // X alignment within cell
251
- switch (alignment) {
252
- case 'center':
253
- item.x = offsetX + col * cellW + (maxItemWidth - size.width) / 2;
254
- break;
255
- case 'end':
256
- item.x = offsetX + col * cellW + maxItemWidth - size.width;
257
- break;
258
- default:
259
- item.x = offsetX + col * cellW;
260
- }
261
-
262
- item.y = offsetY + row * cellH;
263
- }
264
- }
265
-
266
- private layoutWrap(
267
- maxWidth: number,
268
- gap: number,
269
- alignment: LayoutAlignment,
270
- offsetX: number,
271
- offsetY: number,
272
- ): void {
273
- let x = offsetX;
274
- let y = offsetY;
275
- let rowHeight = 0;
276
- const sizes = this._items.map(item => this.getItemSize(item));
277
-
278
- for (let i = 0; i < this._items.length; i++) {
279
- const item = this._items[i];
280
- const size = sizes[i];
281
-
282
- // Check if item fits in current row
283
- if (x + size.width > maxWidth + offsetX && x > offsetX) {
284
- // Wrap to next row
285
- x = offsetX;
286
- y += rowHeight + gap;
287
- rowHeight = 0;
288
- }
289
-
290
- item.x = x;
291
- item.y = y;
292
-
293
- x += size.width + gap;
294
- rowHeight = Math.max(rowHeight, size.height);
228
+ private applyGridChildWidth(child: Container): void {
229
+ const effective = this.resolveConfig();
230
+ const columns = effective.columns ?? this._layoutConfig.columns;
231
+ const gap = effective.gap ?? this._layoutConfig.gap;
232
+
233
+ // Account for gaps between columns: total gap space = gap * (columns - 1)
234
+ // Each column gets: (100% - total_gap) / columns
235
+ // We use flexBasis + flexGrow to let Yoga handle the math when gap > 0
236
+ const styles: Record<string, unknown> = gap > 0
237
+ ? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
238
+ : { width: `${(100 / columns).toFixed(2)}%` };
239
+
240
+ if (child._layout) {
241
+ child._layout.setStyle(styles);
242
+ } else {
243
+ child.layout = styles;
295
244
  }
296
245
  }
297
246
 
298
- private applyAnchor(anchor: LayoutAnchor): void {
247
+ private applyAnchor(): void {
248
+ const anchor = this.resolveConfig().anchor ?? this._anchor;
299
249
  if (this._viewportWidth === 0 || this._viewportHeight === 0) return;
300
250
 
301
- const bounds = this._content.getBounds();
302
- const contentW = bounds.width;
303
- const contentH = bounds.height;
251
+ const bounds = this.getLocalBounds();
252
+ const contentW = bounds.width * this.scale.x;
253
+ const contentH = bounds.height * this.scale.y;
304
254
  const vw = this._viewportWidth;
305
255
  const vh = this._viewportHeight;
306
256
 
307
257
  let anchorX = 0;
308
258
  let anchorY = 0;
309
259
 
310
- // Horizontal
311
260
  if (anchor.includes('left')) {
312
261
  anchorX = 0;
313
262
  } else if (anchor.includes('right')) {
314
263
  anchorX = vw - contentW;
315
264
  } else {
316
- // center
317
265
  anchorX = (vw - contentW) / 2;
318
266
  }
319
267
 
320
- // Vertical
321
268
  if (anchor.startsWith('top')) {
322
269
  anchorY = 0;
323
270
  } else if (anchor.startsWith('bottom')) {
324
271
  anchorY = vh - contentH;
325
272
  } else {
326
- // center
327
273
  anchorY = (vh - contentH) / 2;
328
274
  }
329
275
 
330
- // Compensate for content's local bounds offset
331
- this.x = anchorX - bounds.x;
332
- this.y = anchorY - bounds.y;
276
+ this.x = anchorX - bounds.x * this.scale.x;
277
+ this.y = anchorY - bounds.y * this.scale.y;
333
278
  }
334
279
 
335
280
  private resolveConfig(): Partial<LayoutConfig> {
@@ -337,28 +282,11 @@ export class Layout extends Container {
337
282
  return {};
338
283
  }
339
284
 
340
- // Find the largest breakpoint that's ≤ current viewport width
341
- let resolved: Partial<LayoutConfig> = {};
342
285
  for (const [maxWidth, overrides] of this._breakpoints) {
343
286
  if (this._viewportWidth <= maxWidth) {
344
- resolved = overrides;
345
- break;
287
+ return overrides;
346
288
  }
347
289
  }
348
- return resolved;
349
- }
350
-
351
- private getItemSize(item: Container): { width: number; height: number } {
352
- const bounds = item.getBounds();
353
- return { width: bounds.width, height: bounds.height };
354
- }
355
-
356
- private static normalizePadding(
357
- padding: number | [number, number, number, number],
358
- ): [number, number, number, number] {
359
- if (typeof padding === 'number') {
360
- return [padding, padding, padding, padding];
361
- }
362
- return padding;
290
+ return {};
363
291
  }
364
292
  }