@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 +71 -2
- package/dist/index.cjs +234 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -46
- package/dist/index.d.ts +76 -46
- package/dist/index.js +234 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
{ ...
|
|
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
|
-
{ ...
|
|
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
|
|
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
|
-
|
|
558
|
+
segmentY,
|
|
539
559
|
segmentWidth,
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
|
1402
|
+
this.collapseFloorSegment(i, `pressure ${pressure} >= threshold ${threshold}`);
|
|
1380
1403
|
}
|
|
1381
1404
|
}
|
|
1382
1405
|
}
|
|
1383
1406
|
/** Collapse a single floor segment */
|
|
1384
|
-
collapseFloorSegment(index,
|
|
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
|
-
|
|
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.
|
|
1989
|
-
*
|
|
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
|
-
|
|
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 (
|
|
2282
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2283
|
+
const line = lines[lineIndex];
|
|
2142
2284
|
const chars = line.split("");
|
|
2143
|
-
|
|
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 (
|
|
2340
|
-
|
|
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
|
/**
|