@blorkfield/overlay-core 0.5.0 → 0.5.1
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 -29
- 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 -29
- 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() {
|
|
@@ -1642,6 +1698,7 @@ var OverlayScene = class {
|
|
|
1642
1698
|
this.collapsedSegments.clear();
|
|
1643
1699
|
this.floorSegmentPressure.clear();
|
|
1644
1700
|
import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
|
|
1701
|
+
this.checkInitialFloorIntegrity();
|
|
1645
1702
|
this.render.options.width = width;
|
|
1646
1703
|
this.render.options.height = height;
|
|
1647
1704
|
this.render.canvas.width = width;
|
|
@@ -1658,6 +1715,21 @@ var OverlayScene = class {
|
|
|
1658
1715
|
* Without 'falling' tag, object is static.
|
|
1659
1716
|
*/
|
|
1660
1717
|
spawnObject(config) {
|
|
1718
|
+
if (config.element) {
|
|
1719
|
+
const result = this.addDOMObstacleInternal({
|
|
1720
|
+
element: config.element,
|
|
1721
|
+
x: config.x,
|
|
1722
|
+
y: config.y,
|
|
1723
|
+
width: config.width,
|
|
1724
|
+
height: config.height,
|
|
1725
|
+
tags: config.tags,
|
|
1726
|
+
pressureThreshold: config.pressureThreshold,
|
|
1727
|
+
weight: config.weight,
|
|
1728
|
+
shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
|
|
1729
|
+
clickToFall: config.clickToFall
|
|
1730
|
+
});
|
|
1731
|
+
return result.id;
|
|
1732
|
+
}
|
|
1661
1733
|
const id = crypto.randomUUID();
|
|
1662
1734
|
const tags = config.tags ?? [];
|
|
1663
1735
|
const isStatic = !tags.includes("falling");
|
|
@@ -1677,6 +1749,17 @@ var OverlayScene = class {
|
|
|
1677
1749
|
} else {
|
|
1678
1750
|
body = createObstacle(id, config, isStatic);
|
|
1679
1751
|
}
|
|
1752
|
+
let pressureThreshold;
|
|
1753
|
+
if (config.pressureThreshold) {
|
|
1754
|
+
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
1755
|
+
}
|
|
1756
|
+
let shadow;
|
|
1757
|
+
if (config.shadow === true) {
|
|
1758
|
+
shadow = { opacity: 0.3 };
|
|
1759
|
+
} else if (config.shadow && typeof config.shadow === "object") {
|
|
1760
|
+
shadow = { opacity: config.shadow.opacity ?? 0.3 };
|
|
1761
|
+
}
|
|
1762
|
+
const clicksRemaining = config.clickToFall?.clicks;
|
|
1680
1763
|
const entry = {
|
|
1681
1764
|
id,
|
|
1682
1765
|
body,
|
|
@@ -1684,10 +1767,17 @@ var OverlayScene = class {
|
|
|
1684
1767
|
spawnTime: performance.now(),
|
|
1685
1768
|
ttl: config.ttl,
|
|
1686
1769
|
despawnEffect: config.despawnEffect,
|
|
1687
|
-
weight: config.weight ?? 1
|
|
1770
|
+
weight: config.weight ?? 1,
|
|
1771
|
+
pressureThreshold,
|
|
1772
|
+
shadow,
|
|
1773
|
+
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
1774
|
+
clicksRemaining
|
|
1688
1775
|
};
|
|
1689
1776
|
this.objects.set(id, entry);
|
|
1690
1777
|
import_matter_js5.default.Composite.add(this.engine.world, body);
|
|
1778
|
+
if (isStatic && pressureThreshold !== void 0) {
|
|
1779
|
+
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
1780
|
+
}
|
|
1691
1781
|
return id;
|
|
1692
1782
|
}
|
|
1693
1783
|
/**
|
|
@@ -1714,6 +1804,17 @@ var OverlayScene = class {
|
|
|
1714
1804
|
} else {
|
|
1715
1805
|
body = await createObstacleAsync(id, config, isStatic);
|
|
1716
1806
|
}
|
|
1807
|
+
let pressureThreshold;
|
|
1808
|
+
if (config.pressureThreshold) {
|
|
1809
|
+
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
1810
|
+
}
|
|
1811
|
+
let shadow;
|
|
1812
|
+
if (config.shadow === true) {
|
|
1813
|
+
shadow = { opacity: 0.3 };
|
|
1814
|
+
} else if (config.shadow && typeof config.shadow === "object") {
|
|
1815
|
+
shadow = { opacity: config.shadow.opacity ?? 0.3 };
|
|
1816
|
+
}
|
|
1817
|
+
const clicksRemaining = config.clickToFall?.clicks;
|
|
1717
1818
|
const entry = {
|
|
1718
1819
|
id,
|
|
1719
1820
|
body,
|
|
@@ -1721,10 +1822,17 @@ var OverlayScene = class {
|
|
|
1721
1822
|
spawnTime: performance.now(),
|
|
1722
1823
|
ttl: config.ttl,
|
|
1723
1824
|
despawnEffect: config.despawnEffect,
|
|
1724
|
-
weight: config.weight ?? 1
|
|
1825
|
+
weight: config.weight ?? 1,
|
|
1826
|
+
pressureThreshold,
|
|
1827
|
+
shadow,
|
|
1828
|
+
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
1829
|
+
clicksRemaining
|
|
1725
1830
|
};
|
|
1726
1831
|
this.objects.set(id, entry);
|
|
1727
1832
|
import_matter_js5.default.Composite.add(this.engine.world, body);
|
|
1833
|
+
if (isStatic && pressureThreshold !== void 0) {
|
|
1834
|
+
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
1835
|
+
}
|
|
1728
1836
|
return id;
|
|
1729
1837
|
}
|
|
1730
1838
|
/**
|
|
@@ -1983,17 +2091,12 @@ var OverlayScene = class {
|
|
|
1983
2091
|
areFontsInitialized() {
|
|
1984
2092
|
return this.fontsInitialized;
|
|
1985
2093
|
}
|
|
1986
|
-
// ==================== DOM OBSTACLE METHODS ====================
|
|
2094
|
+
// ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
|
|
1987
2095
|
/**
|
|
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.
|
|
2096
|
+
* Internal: Attach a DOM element to physics.
|
|
2097
|
+
* Called by spawnObject when element is provided.
|
|
1995
2098
|
*/
|
|
1996
|
-
|
|
2099
|
+
addDOMObstacleInternal(config) {
|
|
1997
2100
|
const { element, x, y } = config;
|
|
1998
2101
|
const width = config.width ?? element.offsetWidth;
|
|
1999
2102
|
const height = config.height ?? element.offsetHeight;
|
|
@@ -2030,6 +2133,7 @@ var OverlayScene = class {
|
|
|
2030
2133
|
};
|
|
2031
2134
|
this.objects.set(id, entry);
|
|
2032
2135
|
import_matter_js5.default.Composite.add(this.engine.world, body);
|
|
2136
|
+
this.updateDOMElementTransform(entry);
|
|
2033
2137
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2034
2138
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2035
2139
|
}
|
|
@@ -2136,11 +2240,61 @@ var OverlayScene = class {
|
|
|
2136
2240
|
maxDimension = Math.max(maxDimension, dims.width, dims.height);
|
|
2137
2241
|
}
|
|
2138
2242
|
if (maxDimension === 0) maxDimension = 100;
|
|
2243
|
+
const calculateLineWidth = (line) => {
|
|
2244
|
+
let width = 0;
|
|
2245
|
+
for (const char of line) {
|
|
2246
|
+
if (char === " ") {
|
|
2247
|
+
width += 20;
|
|
2248
|
+
} else if (/^[A-Za-z0-9]$/.test(char)) {
|
|
2249
|
+
const dims = charDimensions.get(char);
|
|
2250
|
+
if (dims) {
|
|
2251
|
+
const scale = letterSize / Math.max(dims.width, dims.height);
|
|
2252
|
+
const scaledWidth = dims.width * scale;
|
|
2253
|
+
const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
|
|
2254
|
+
width += scaledWidth + Math.max(0, extraSpacing);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return width;
|
|
2259
|
+
};
|
|
2260
|
+
const lineWidths = lines.map((line) => calculateLineWidth(line));
|
|
2261
|
+
const align = config.align ?? "left";
|
|
2262
|
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
|
2263
|
+
let boundsLeft;
|
|
2264
|
+
let boundsRight;
|
|
2265
|
+
switch (align) {
|
|
2266
|
+
case "center":
|
|
2267
|
+
boundsLeft = config.x - maxLineWidth / 2;
|
|
2268
|
+
boundsRight = config.x + maxLineWidth / 2;
|
|
2269
|
+
break;
|
|
2270
|
+
case "right":
|
|
2271
|
+
boundsLeft = config.x - maxLineWidth;
|
|
2272
|
+
boundsRight = config.x;
|
|
2273
|
+
break;
|
|
2274
|
+
default:
|
|
2275
|
+
boundsLeft = config.x;
|
|
2276
|
+
boundsRight = config.x + maxLineWidth;
|
|
2277
|
+
}
|
|
2278
|
+
const boundsTop = config.y - letterSize / 2;
|
|
2279
|
+
const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
|
|
2280
|
+
const boundsBottom = boundsTop + totalHeight;
|
|
2139
2281
|
let currentY = config.y;
|
|
2140
2282
|
let globalCharIndex = 0;
|
|
2141
|
-
for (
|
|
2283
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2284
|
+
const line = lines[lineIndex];
|
|
2142
2285
|
const chars = line.split("");
|
|
2143
|
-
|
|
2286
|
+
const lineWidth = lineWidths[lineIndex];
|
|
2287
|
+
let currentX;
|
|
2288
|
+
switch (align) {
|
|
2289
|
+
case "center":
|
|
2290
|
+
currentX = config.x - lineWidth / 2;
|
|
2291
|
+
break;
|
|
2292
|
+
case "right":
|
|
2293
|
+
currentX = config.x - lineWidth;
|
|
2294
|
+
break;
|
|
2295
|
+
default:
|
|
2296
|
+
currentX = config.x;
|
|
2297
|
+
}
|
|
2144
2298
|
if (inWord) {
|
|
2145
2299
|
currentWordIndex++;
|
|
2146
2300
|
inWord = false;
|
|
@@ -2256,12 +2410,21 @@ var OverlayScene = class {
|
|
|
2256
2410
|
letterColor,
|
|
2257
2411
|
lineCount: lines.length
|
|
2258
2412
|
});
|
|
2413
|
+
const bounds = {
|
|
2414
|
+
left: boundsLeft,
|
|
2415
|
+
right: boundsRight,
|
|
2416
|
+
top: boundsTop,
|
|
2417
|
+
bottom: boundsBottom,
|
|
2418
|
+
width: boundsRight - boundsLeft,
|
|
2419
|
+
height: boundsBottom - boundsTop
|
|
2420
|
+
};
|
|
2259
2421
|
return {
|
|
2260
2422
|
letterIds,
|
|
2261
2423
|
stringTag,
|
|
2262
2424
|
wordTags,
|
|
2263
2425
|
letterMap,
|
|
2264
|
-
letterDebugInfo: debugInfo
|
|
2426
|
+
letterDebugInfo: debugInfo,
|
|
2427
|
+
bounds
|
|
2265
2428
|
};
|
|
2266
2429
|
}
|
|
2267
2430
|
/**
|
|
@@ -2334,10 +2497,43 @@ var OverlayScene = class {
|
|
|
2334
2497
|
const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
|
|
2335
2498
|
const fontFamily = fontInfo?.name ?? "sans-serif";
|
|
2336
2499
|
const lines = text.split("\n");
|
|
2500
|
+
const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
|
|
2501
|
+
const align = config.align ?? "left";
|
|
2502
|
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
|
2503
|
+
let boundsLeft;
|
|
2504
|
+
let boundsRight;
|
|
2505
|
+
switch (align) {
|
|
2506
|
+
case "center":
|
|
2507
|
+
boundsLeft = x - maxLineWidth / 2;
|
|
2508
|
+
boundsRight = x + maxLineWidth / 2;
|
|
2509
|
+
break;
|
|
2510
|
+
case "right":
|
|
2511
|
+
boundsLeft = x - maxLineWidth;
|
|
2512
|
+
boundsRight = x;
|
|
2513
|
+
break;
|
|
2514
|
+
default:
|
|
2515
|
+
boundsLeft = x;
|
|
2516
|
+
boundsRight = x + maxLineWidth;
|
|
2517
|
+
}
|
|
2518
|
+
const boundsTop = y - fontSize * 0.8;
|
|
2519
|
+
const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
|
|
2520
|
+
const boundsBottom = boundsTop + totalHeight;
|
|
2337
2521
|
let currentY = y;
|
|
2338
2522
|
let globalCharIndex = 0;
|
|
2339
|
-
for (
|
|
2340
|
-
|
|
2523
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2524
|
+
const line = lines[lineIndex];
|
|
2525
|
+
const lineWidth = lineWidths[lineIndex];
|
|
2526
|
+
let currentX;
|
|
2527
|
+
switch (align) {
|
|
2528
|
+
case "center":
|
|
2529
|
+
currentX = x - lineWidth / 2;
|
|
2530
|
+
break;
|
|
2531
|
+
case "right":
|
|
2532
|
+
currentX = x - lineWidth;
|
|
2533
|
+
break;
|
|
2534
|
+
default:
|
|
2535
|
+
currentX = x;
|
|
2536
|
+
}
|
|
2341
2537
|
if (inWord) {
|
|
2342
2538
|
currentWordIndex++;
|
|
2343
2539
|
inWord = false;
|
|
@@ -2447,12 +2643,21 @@ var OverlayScene = class {
|
|
|
2447
2643
|
wordTags,
|
|
2448
2644
|
lineCount: lines.length
|
|
2449
2645
|
});
|
|
2646
|
+
const bounds = {
|
|
2647
|
+
left: boundsLeft,
|
|
2648
|
+
right: boundsRight,
|
|
2649
|
+
top: boundsTop,
|
|
2650
|
+
bottom: boundsBottom,
|
|
2651
|
+
width: boundsRight - boundsLeft,
|
|
2652
|
+
height: boundsBottom - boundsTop
|
|
2653
|
+
};
|
|
2450
2654
|
return {
|
|
2451
2655
|
letterIds,
|
|
2452
2656
|
stringTag,
|
|
2453
2657
|
wordTags,
|
|
2454
2658
|
letterMap,
|
|
2455
|
-
letterDebugInfo: []
|
|
2659
|
+
letterDebugInfo: [],
|
|
2660
|
+
bounds
|
|
2456
2661
|
};
|
|
2457
2662
|
}
|
|
2458
2663
|
/**
|