@blorkfield/overlay-core 0.5.0 → 0.5.2

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/README.md CHANGED
@@ -48,6 +48,15 @@ Static obstacles track how many dynamic objects are resting on them. When the ac
48
48
 
49
49
  The floor can be divided into independent segments, each with its own pressure threshold. When a segment receives too much weight, it collapses and objects fall through.
50
50
 
51
+ | Option | Description |
52
+ |--------|-------------|
53
+ | `thickness` | Segment height in pixels (single value or array per segment) |
54
+ | `color` | Segment fill color (single value or array per segment) - makes floor visible |
55
+ | `minIntegrity` | Minimum segments required. When remaining segments drop below this, all collapse |
56
+ | `segmentWidths` | Proportional widths for each segment (array that sums to 1.0, e.g., `[0.2, 0.3, 0.5]`) |
57
+
58
+ Example: With 10 segments and `minIntegrity: 7`, once 4 segments have collapsed (leaving 6), all remaining segments collapse together.
59
+
51
60
  ## Quick Start
52
61
 
53
62
  ```typescript
@@ -72,6 +81,8 @@ scene.start();
72
81
 
73
82
  ## Spawning Objects
74
83
 
84
+ All objects are created through `spawnObject()` (or `spawnObjectAsync()` for images). The same config supports canvas-rendered shapes, image-based shapes, and DOM elements.
85
+
75
86
  ### Basic Shapes
76
87
 
77
88
  ```typescript
@@ -118,6 +129,57 @@ const id = await scene.spawnObjectAsync({
118
129
  });
119
130
  ```
120
131
 
132
+ ### DOM Elements
133
+
134
+ Pass a DOM element via the `element` property to link it to physics. The element will move with the physics body when it becomes dynamic.
135
+
136
+ ```typescript
137
+ const contentBox = document.getElementById('content-box');
138
+
139
+ scene.spawnObject({
140
+ element: contentBox,
141
+ x: boxX,
142
+ y: boxY,
143
+ width: contentBox.offsetWidth,
144
+ height: contentBox.offsetHeight,
145
+ tags: ['grabable'],
146
+ pressureThreshold: { value: 50 },
147
+ shadow: { opacity: 0.3 },
148
+ clickToFall: { clicks: 5 }
149
+ });
150
+ ```
151
+
152
+ When a DOM element collapses:
153
+ - The element's CSS transform is updated each frame to follow physics
154
+ - Shadow creates a cloned DOM element at the original position
155
+
156
+ ### Pressure, Shadow, and Click Behavior
157
+
158
+ These options work on any spawned object (shapes, images, or DOM elements):
159
+
160
+ ```typescript
161
+ scene.spawnObject({
162
+ x: 200,
163
+ y: 300,
164
+ width: 150,
165
+ height: 30,
166
+ fillStyle: '#333',
167
+ tags: ['grabable'],
168
+
169
+ // Collapse when 20 units of pressure accumulate
170
+ pressureThreshold: { value: 20 },
171
+
172
+ // This object contributes 5 pressure when resting on something
173
+ weight: 5,
174
+
175
+ // Leave a faded copy when collapsed (true = 0.3 opacity default)
176
+ shadow: { opacity: 0.3 },
177
+
178
+ // Collapse after being clicked 3 times
179
+ clickToFall: { clicks: 3 }
180
+ });
181
+ ```
182
+
121
183
  ## Text Obstacles
122
184
 
123
185
  ### PNG Based Text
@@ -263,7 +325,10 @@ const scene = new OverlayScene(canvas, {
263
325
  background: '#16213e',
264
326
  floorConfig: {
265
327
  segments: 10,
266
- threshold: 100
328
+ threshold: 100,
329
+ thickness: 20,
330
+ color: '#3a4a6a', // Makes floor visible
331
+ minIntegrity: 7 // All collapse if fewer than 7 remain
267
332
  },
268
333
  despawnBelowFloor: 1.0
269
334
  });
@@ -276,7 +341,11 @@ const scene = new OverlayScene(canvas, {
276
341
  | `debug` | false | Show collision wireframes |
277
342
  | `background` | transparent | Canvas background color |
278
343
  | `floorConfig.segments` | 1 | Number of floor segments |
279
- | `floorConfig.threshold` | none | Pressure threshold for floor collapse |
344
+ | `floorConfig.threshold` | none | Pressure threshold for collapse (number or array per segment) |
345
+ | `floorConfig.thickness` | 50 | Floor thickness in pixels (number or array per segment) |
346
+ | `floorConfig.color` | none | Floor color - makes segments visible (string or array per segment) |
347
+ | `floorConfig.minIntegrity` | none | Minimum segments required, otherwise all collapse |
348
+ | `floorConfig.segmentWidths` | none | Proportional widths for each segment (array that sums to 1.0) |
280
349
  | `despawnBelowFloor` | 1.0 | Distance below floor to despawn objects (as fraction of height) |
281
350
 
282
351
  ## Pressure Tracking
package/dist/index.cjs CHANGED
@@ -508,7 +508,7 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
508
508
  function createBoundariesWithFloorConfig(bounds, floorConfig) {
509
509
  const width = bounds.right - bounds.left;
510
510
  const height = bounds.bottom - bounds.top;
511
- const options = { isStatic: true, render: { visible: false } };
511
+ const wallOptions = { isStatic: true, render: { visible: false } };
512
512
  const walls = [
513
513
  // Left wall
514
514
  import_matter_js2.default.Bodies.rectangle(
@@ -516,7 +516,7 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
516
516
  bounds.top + height / 2,
517
517
  BOUNDARY_THICKNESS,
518
518
  height,
519
- { ...options, label: "leftWall" }
519
+ { ...wallOptions, label: "leftWall" }
520
520
  ),
521
521
  // Right wall
522
522
  import_matter_js2.default.Bodies.rectangle(
@@ -524,23 +524,44 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
524
524
  bounds.top + height / 2,
525
525
  BOUNDARY_THICKNESS,
526
526
  height,
527
- { ...options, label: "rightWall" }
527
+ { ...wallOptions, label: "rightWall" }
528
528
  )
529
529
  ];
530
530
  const segmentCount = floorConfig?.segments ?? 1;
531
- const segmentWidth = width / segmentCount;
532
531
  const floorSegments = [];
532
+ let segmentWidths;
533
+ if (floorConfig?.segmentWidths && floorConfig.segmentWidths.length === segmentCount) {
534
+ const sum = floorConfig.segmentWidths.reduce((a, b) => a + b, 0);
535
+ segmentWidths = floorConfig.segmentWidths.map((w) => w / sum * width);
536
+ } else {
537
+ const equalWidth = width / segmentCount;
538
+ segmentWidths = Array(segmentCount).fill(equalWidth);
539
+ }
540
+ let currentX = bounds.left;
533
541
  for (let i = 0; i < segmentCount; i++) {
534
- const segmentX = bounds.left + (i + 0.5) * segmentWidth;
542
+ const segmentWidth = segmentWidths[i];
543
+ const thickness = floorConfig?.thickness !== void 0 ? Array.isArray(floorConfig.thickness) ? floorConfig.thickness[i] ?? BOUNDARY_THICKNESS : floorConfig.thickness : BOUNDARY_THICKNESS;
544
+ const color = floorConfig?.color !== void 0 ? Array.isArray(floorConfig.color) ? floorConfig.color[i] : floorConfig.color : void 0;
545
+ const segmentX = currentX + segmentWidth / 2;
546
+ const segmentY = bounds.bottom - thickness / 2;
547
+ const segmentOptions = {
548
+ isStatic: true,
549
+ label: `floor-segment-${i}`,
550
+ render: {
551
+ visible: color !== void 0,
552
+ fillStyle: color ?? "#888888"
553
+ }
554
+ };
535
555
  floorSegments.push(
536
556
  import_matter_js2.default.Bodies.rectangle(
537
557
  segmentX,
538
- bounds.bottom + BOUNDARY_THICKNESS / 2,
558
+ segmentY,
539
559
  segmentWidth,
540
- BOUNDARY_THICKNESS,
541
- { ...options, label: `floor-segment-${i}` }
560
+ thickness,
561
+ segmentOptions
542
562
  )
543
563
  );
564
+ currentX += segmentWidth;
544
565
  }
545
566
  return { walls, floorSegments };
546
567
  }
@@ -1232,6 +1253,7 @@ var OverlayScene = class {
1232
1253
  this.boundaries = [...boundariesResult.walls, ...boundariesResult.floorSegments];
1233
1254
  this.floorSegments = boundariesResult.floorSegments;
1234
1255
  import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
1256
+ this.checkInitialFloorIntegrity();
1235
1257
  this.mouse = import_matter_js5.default.Mouse.create(canvas);
1236
1258
  this.mouseConstraint = import_matter_js5.default.MouseConstraint.create(this.engine, {
1237
1259
  mouse: this.mouse,
@@ -1350,7 +1372,8 @@ var OverlayScene = class {
1350
1372
  if (onObstacles.has(dyn.id)) continue;
1351
1373
  const dynBounds = dyn.body.bounds;
1352
1374
  const horizontalOverlap = dynBounds.max.x > segmentBounds.min.x && dynBounds.min.x < segmentBounds.max.x;
1353
- if (horizontalOverlap) {
1375
+ const nearFloor = dynBounds.max.y >= segmentBounds.min.y - 10;
1376
+ if (horizontalOverlap && nearFloor) {
1354
1377
  resting.add(dyn.id);
1355
1378
  }
1356
1379
  }
@@ -1376,17 +1399,50 @@ var OverlayScene = class {
1376
1399
  const objectIds = this.floorSegmentPressure.get(i);
1377
1400
  const pressure = objectIds ? this.calculateWeightedPressure(objectIds) : 0;
1378
1401
  if (pressure >= threshold) {
1379
- this.collapseFloorSegment(i, pressure, threshold);
1402
+ this.collapseFloorSegment(i, `pressure ${pressure} >= threshold ${threshold}`);
1380
1403
  }
1381
1404
  }
1382
1405
  }
1383
1406
  /** Collapse a single floor segment */
1384
- collapseFloorSegment(index, pressure, threshold) {
1407
+ collapseFloorSegment(index, reason) {
1385
1408
  if (this.collapsedSegments.has(index)) return;
1386
1409
  this.collapsedSegments.add(index);
1387
1410
  const segment = this.floorSegments[index];
1388
1411
  import_matter_js5.default.Composite.remove(this.engine.world, segment);
1389
- console.log(`[Pressure] Floor segment ${index} collapsed! (pressure: ${pressure} >= ${threshold})`);
1412
+ logger.debug("OverlayScene", `Floor segment ${index} collapsed: ${reason}`);
1413
+ this.checkFloorIntegrity();
1414
+ }
1415
+ /** Check if floor integrity requirement is violated and collapse all remaining if so */
1416
+ checkFloorIntegrity() {
1417
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1418
+ if (minIntegrity === void 0) return;
1419
+ const totalSegments = this.floorSegments.length;
1420
+ const remainingSegments = totalSegments - this.collapsedSegments.size;
1421
+ if (remainingSegments < minIntegrity && remainingSegments > 0) {
1422
+ logger.debug("OverlayScene", `Floor integrity failed: ${remainingSegments} remaining < ${minIntegrity} required. Collapsing all.`);
1423
+ for (let i = 0; i < totalSegments; i++) {
1424
+ if (!this.collapsedSegments.has(i)) {
1425
+ this.collapsedSegments.add(i);
1426
+ const segment = this.floorSegments[i];
1427
+ import_matter_js5.default.Composite.remove(this.engine.world, segment);
1428
+ logger.debug("OverlayScene", `Floor segment ${i} collapsed: integrity failure cascade`);
1429
+ }
1430
+ }
1431
+ }
1432
+ }
1433
+ /** Check floor integrity on initialization (handles minIntegrity > segments) */
1434
+ checkInitialFloorIntegrity() {
1435
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1436
+ if (minIntegrity === void 0) return;
1437
+ const totalSegments = this.floorSegments.length;
1438
+ if (totalSegments < minIntegrity) {
1439
+ logger.debug("OverlayScene", `Floor integrity impossible: ${totalSegments} segments < ${minIntegrity} required. Collapsing all immediately.`);
1440
+ for (let i = 0; i < totalSegments; i++) {
1441
+ this.collapsedSegments.add(i);
1442
+ const segment = this.floorSegments[i];
1443
+ import_matter_js5.default.Composite.remove(this.engine.world, segment);
1444
+ }
1445
+ }
1390
1446
  }
1391
1447
  /** Log a summary of pressure on all obstacles, grouped by word */
1392
1448
  logPressureSummary() {
@@ -1505,7 +1561,6 @@ var OverlayScene = class {
1505
1561
  const shadowElement = entry.domElement.cloneNode(true);
1506
1562
  shadowElement.style.opacity = String(opacity);
1507
1563
  shadowElement.style.pointerEvents = "none";
1508
- shadowElement.style.transform = entry.domOriginalTransform || "";
1509
1564
  entry.domElement.parentNode?.insertBefore(shadowElement, entry.domElement);
1510
1565
  entry.domShadowElement = shadowElement;
1511
1566
  return;
@@ -1642,6 +1697,7 @@ var OverlayScene = class {
1642
1697
  this.collapsedSegments.clear();
1643
1698
  this.floorSegmentPressure.clear();
1644
1699
  import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
1700
+ this.checkInitialFloorIntegrity();
1645
1701
  this.render.options.width = width;
1646
1702
  this.render.options.height = height;
1647
1703
  this.render.canvas.width = width;
@@ -1658,6 +1714,21 @@ var OverlayScene = class {
1658
1714
  * Without 'falling' tag, object is static.
1659
1715
  */
1660
1716
  spawnObject(config) {
1717
+ if (config.element) {
1718
+ const result = this.addDOMObstacleInternal({
1719
+ element: config.element,
1720
+ x: config.x,
1721
+ y: config.y,
1722
+ width: config.width,
1723
+ height: config.height,
1724
+ tags: config.tags,
1725
+ pressureThreshold: config.pressureThreshold,
1726
+ weight: config.weight,
1727
+ shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
1728
+ clickToFall: config.clickToFall
1729
+ });
1730
+ return result.id;
1731
+ }
1661
1732
  const id = crypto.randomUUID();
1662
1733
  const tags = config.tags ?? [];
1663
1734
  const isStatic = !tags.includes("falling");
@@ -1677,6 +1748,17 @@ var OverlayScene = class {
1677
1748
  } else {
1678
1749
  body = createObstacle(id, config, isStatic);
1679
1750
  }
1751
+ let pressureThreshold;
1752
+ if (config.pressureThreshold) {
1753
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1754
+ }
1755
+ let shadow;
1756
+ if (config.shadow === true) {
1757
+ shadow = { opacity: 0.3 };
1758
+ } else if (config.shadow && typeof config.shadow === "object") {
1759
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1760
+ }
1761
+ const clicksRemaining = config.clickToFall?.clicks;
1680
1762
  const entry = {
1681
1763
  id,
1682
1764
  body,
@@ -1684,10 +1766,17 @@ var OverlayScene = class {
1684
1766
  spawnTime: performance.now(),
1685
1767
  ttl: config.ttl,
1686
1768
  despawnEffect: config.despawnEffect,
1687
- weight: config.weight ?? 1
1769
+ weight: config.weight ?? 1,
1770
+ pressureThreshold,
1771
+ shadow,
1772
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1773
+ clicksRemaining
1688
1774
  };
1689
1775
  this.objects.set(id, entry);
1690
1776
  import_matter_js5.default.Composite.add(this.engine.world, body);
1777
+ if (isStatic && pressureThreshold !== void 0) {
1778
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1779
+ }
1691
1780
  return id;
1692
1781
  }
1693
1782
  /**
@@ -1714,6 +1803,17 @@ var OverlayScene = class {
1714
1803
  } else {
1715
1804
  body = await createObstacleAsync(id, config, isStatic);
1716
1805
  }
1806
+ let pressureThreshold;
1807
+ if (config.pressureThreshold) {
1808
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1809
+ }
1810
+ let shadow;
1811
+ if (config.shadow === true) {
1812
+ shadow = { opacity: 0.3 };
1813
+ } else if (config.shadow && typeof config.shadow === "object") {
1814
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1815
+ }
1816
+ const clicksRemaining = config.clickToFall?.clicks;
1717
1817
  const entry = {
1718
1818
  id,
1719
1819
  body,
@@ -1721,10 +1821,17 @@ var OverlayScene = class {
1721
1821
  spawnTime: performance.now(),
1722
1822
  ttl: config.ttl,
1723
1823
  despawnEffect: config.despawnEffect,
1724
- weight: config.weight ?? 1
1824
+ weight: config.weight ?? 1,
1825
+ pressureThreshold,
1826
+ shadow,
1827
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1828
+ clicksRemaining
1725
1829
  };
1726
1830
  this.objects.set(id, entry);
1727
1831
  import_matter_js5.default.Composite.add(this.engine.world, body);
1832
+ if (isStatic && pressureThreshold !== void 0) {
1833
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1834
+ }
1728
1835
  return id;
1729
1836
  }
1730
1837
  /**
@@ -1983,17 +2090,12 @@ var OverlayScene = class {
1983
2090
  areFontsInitialized() {
1984
2091
  return this.fontsInitialized;
1985
2092
  }
1986
- // ==================== DOM OBSTACLE METHODS ====================
2093
+ // ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
1987
2094
  /**
1988
- * Attach a DOM element to physics. The element will follow the physics body
1989
- * and can have pressure threshold, shadow, and click-to-fall behavior.
1990
- *
1991
- * When the element collapses (becomes dynamic), its CSS transform will be
1992
- * updated each frame to match the physics body position and rotation.
1993
- *
1994
- * Shadow creates a cloned DOM element that stays at the original position.
2095
+ * Internal: Attach a DOM element to physics.
2096
+ * Called by spawnObject when element is provided.
1995
2097
  */
1996
- addDOMObstacle(config) {
2098
+ addDOMObstacleInternal(config) {
1997
2099
  const { element, x, y } = config;
1998
2100
  const width = config.width ?? element.offsetWidth;
1999
2101
  const height = config.height ?? element.offsetHeight;
@@ -2030,6 +2132,7 @@ var OverlayScene = class {
2030
2132
  };
2031
2133
  this.objects.set(id, entry);
2032
2134
  import_matter_js5.default.Composite.add(this.engine.world, body);
2135
+ this.updateDOMElementTransform(entry);
2033
2136
  if (isStatic && pressureThreshold !== void 0) {
2034
2137
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2035
2138
  }
@@ -2136,11 +2239,61 @@ var OverlayScene = class {
2136
2239
  maxDimension = Math.max(maxDimension, dims.width, dims.height);
2137
2240
  }
2138
2241
  if (maxDimension === 0) maxDimension = 100;
2242
+ const calculateLineWidth = (line) => {
2243
+ let width = 0;
2244
+ for (const char of line) {
2245
+ if (char === " ") {
2246
+ width += 20;
2247
+ } else if (/^[A-Za-z0-9]$/.test(char)) {
2248
+ const dims = charDimensions.get(char);
2249
+ if (dims) {
2250
+ const scale = letterSize / Math.max(dims.width, dims.height);
2251
+ const scaledWidth = dims.width * scale;
2252
+ const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
2253
+ width += scaledWidth + Math.max(0, extraSpacing);
2254
+ }
2255
+ }
2256
+ }
2257
+ return width;
2258
+ };
2259
+ const lineWidths = lines.map((line) => calculateLineWidth(line));
2260
+ const align = config.align ?? "left";
2261
+ const maxLineWidth = Math.max(...lineWidths, 0);
2262
+ let boundsLeft;
2263
+ let boundsRight;
2264
+ switch (align) {
2265
+ case "center":
2266
+ boundsLeft = config.x - maxLineWidth / 2;
2267
+ boundsRight = config.x + maxLineWidth / 2;
2268
+ break;
2269
+ case "right":
2270
+ boundsLeft = config.x - maxLineWidth;
2271
+ boundsRight = config.x;
2272
+ break;
2273
+ default:
2274
+ boundsLeft = config.x;
2275
+ boundsRight = config.x + maxLineWidth;
2276
+ }
2277
+ const boundsTop = config.y - letterSize / 2;
2278
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
2279
+ const boundsBottom = boundsTop + totalHeight;
2139
2280
  let currentY = config.y;
2140
2281
  let globalCharIndex = 0;
2141
- for (const line of lines) {
2282
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2283
+ const line = lines[lineIndex];
2142
2284
  const chars = line.split("");
2143
- let currentX = config.x;
2285
+ const lineWidth = lineWidths[lineIndex];
2286
+ let currentX;
2287
+ switch (align) {
2288
+ case "center":
2289
+ currentX = config.x - lineWidth / 2;
2290
+ break;
2291
+ case "right":
2292
+ currentX = config.x - lineWidth;
2293
+ break;
2294
+ default:
2295
+ currentX = config.x;
2296
+ }
2144
2297
  if (inWord) {
2145
2298
  currentWordIndex++;
2146
2299
  inWord = false;
@@ -2256,12 +2409,21 @@ var OverlayScene = class {
2256
2409
  letterColor,
2257
2410
  lineCount: lines.length
2258
2411
  });
2412
+ const bounds = {
2413
+ left: boundsLeft,
2414
+ right: boundsRight,
2415
+ top: boundsTop,
2416
+ bottom: boundsBottom,
2417
+ width: boundsRight - boundsLeft,
2418
+ height: boundsBottom - boundsTop
2419
+ };
2259
2420
  return {
2260
2421
  letterIds,
2261
2422
  stringTag,
2262
2423
  wordTags,
2263
2424
  letterMap,
2264
- letterDebugInfo: debugInfo
2425
+ letterDebugInfo: debugInfo,
2426
+ bounds
2265
2427
  };
2266
2428
  }
2267
2429
  /**
@@ -2334,10 +2496,43 @@ var OverlayScene = class {
2334
2496
  const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
2335
2497
  const fontFamily = fontInfo?.name ?? "sans-serif";
2336
2498
  const lines = text.split("\n");
2499
+ const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
2500
+ const align = config.align ?? "left";
2501
+ const maxLineWidth = Math.max(...lineWidths, 0);
2502
+ let boundsLeft;
2503
+ let boundsRight;
2504
+ switch (align) {
2505
+ case "center":
2506
+ boundsLeft = x - maxLineWidth / 2;
2507
+ boundsRight = x + maxLineWidth / 2;
2508
+ break;
2509
+ case "right":
2510
+ boundsLeft = x - maxLineWidth;
2511
+ boundsRight = x;
2512
+ break;
2513
+ default:
2514
+ boundsLeft = x;
2515
+ boundsRight = x + maxLineWidth;
2516
+ }
2517
+ const boundsTop = y - fontSize * 0.8;
2518
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
2519
+ const boundsBottom = boundsTop + totalHeight;
2337
2520
  let currentY = y;
2338
2521
  let globalCharIndex = 0;
2339
- for (const line of lines) {
2340
- let currentX = x;
2522
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2523
+ const line = lines[lineIndex];
2524
+ const lineWidth = lineWidths[lineIndex];
2525
+ let currentX;
2526
+ switch (align) {
2527
+ case "center":
2528
+ currentX = x - lineWidth / 2;
2529
+ break;
2530
+ case "right":
2531
+ currentX = x - lineWidth;
2532
+ break;
2533
+ default:
2534
+ currentX = x;
2535
+ }
2341
2536
  if (inWord) {
2342
2537
  currentWordIndex++;
2343
2538
  inWord = false;
@@ -2447,12 +2642,21 @@ var OverlayScene = class {
2447
2642
  wordTags,
2448
2643
  lineCount: lines.length
2449
2644
  });
2645
+ const bounds = {
2646
+ left: boundsLeft,
2647
+ right: boundsRight,
2648
+ top: boundsTop,
2649
+ bottom: boundsBottom,
2650
+ width: boundsRight - boundsLeft,
2651
+ height: boundsBottom - boundsTop
2652
+ };
2450
2653
  return {
2451
2654
  letterIds,
2452
2655
  stringTag,
2453
2656
  wordTags,
2454
2657
  letterMap,
2455
- letterDebugInfo: []
2658
+ letterDebugInfo: [],
2659
+ bounds
2456
2660
  };
2457
2661
  }
2458
2662
  /**