@blorkfield/overlay-core 0.4.3 → 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 +331 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +86 -3
- package/dist/index.js +331 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -464,7 +464,7 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
|
|
|
464
464
|
function createBoundariesWithFloorConfig(bounds, floorConfig) {
|
|
465
465
|
const width = bounds.right - bounds.left;
|
|
466
466
|
const height = bounds.bottom - bounds.top;
|
|
467
|
-
const
|
|
467
|
+
const wallOptions = { isStatic: true, render: { visible: false } };
|
|
468
468
|
const walls = [
|
|
469
469
|
// Left wall
|
|
470
470
|
Matter2.Bodies.rectangle(
|
|
@@ -472,7 +472,7 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
|
|
|
472
472
|
bounds.top + height / 2,
|
|
473
473
|
BOUNDARY_THICKNESS,
|
|
474
474
|
height,
|
|
475
|
-
{ ...
|
|
475
|
+
{ ...wallOptions, label: "leftWall" }
|
|
476
476
|
),
|
|
477
477
|
// Right wall
|
|
478
478
|
Matter2.Bodies.rectangle(
|
|
@@ -480,23 +480,44 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
|
|
|
480
480
|
bounds.top + height / 2,
|
|
481
481
|
BOUNDARY_THICKNESS,
|
|
482
482
|
height,
|
|
483
|
-
{ ...
|
|
483
|
+
{ ...wallOptions, label: "rightWall" }
|
|
484
484
|
)
|
|
485
485
|
];
|
|
486
486
|
const segmentCount = floorConfig?.segments ?? 1;
|
|
487
|
-
const segmentWidth = width / segmentCount;
|
|
488
487
|
const floorSegments = [];
|
|
488
|
+
let segmentWidths;
|
|
489
|
+
if (floorConfig?.segmentWidths && floorConfig.segmentWidths.length === segmentCount) {
|
|
490
|
+
const sum = floorConfig.segmentWidths.reduce((a, b) => a + b, 0);
|
|
491
|
+
segmentWidths = floorConfig.segmentWidths.map((w) => w / sum * width);
|
|
492
|
+
} else {
|
|
493
|
+
const equalWidth = width / segmentCount;
|
|
494
|
+
segmentWidths = Array(segmentCount).fill(equalWidth);
|
|
495
|
+
}
|
|
496
|
+
let currentX = bounds.left;
|
|
489
497
|
for (let i = 0; i < segmentCount; i++) {
|
|
490
|
-
const
|
|
498
|
+
const segmentWidth = segmentWidths[i];
|
|
499
|
+
const thickness = floorConfig?.thickness !== void 0 ? Array.isArray(floorConfig.thickness) ? floorConfig.thickness[i] ?? BOUNDARY_THICKNESS : floorConfig.thickness : BOUNDARY_THICKNESS;
|
|
500
|
+
const color = floorConfig?.color !== void 0 ? Array.isArray(floorConfig.color) ? floorConfig.color[i] : floorConfig.color : void 0;
|
|
501
|
+
const segmentX = currentX + segmentWidth / 2;
|
|
502
|
+
const segmentY = bounds.bottom - thickness / 2;
|
|
503
|
+
const segmentOptions = {
|
|
504
|
+
isStatic: true,
|
|
505
|
+
label: `floor-segment-${i}`,
|
|
506
|
+
render: {
|
|
507
|
+
visible: color !== void 0,
|
|
508
|
+
fillStyle: color ?? "#888888"
|
|
509
|
+
}
|
|
510
|
+
};
|
|
491
511
|
floorSegments.push(
|
|
492
512
|
Matter2.Bodies.rectangle(
|
|
493
513
|
segmentX,
|
|
494
|
-
|
|
514
|
+
segmentY,
|
|
495
515
|
segmentWidth,
|
|
496
|
-
|
|
497
|
-
|
|
516
|
+
thickness,
|
|
517
|
+
segmentOptions
|
|
498
518
|
)
|
|
499
519
|
);
|
|
520
|
+
currentX += segmentWidth;
|
|
500
521
|
}
|
|
501
522
|
return { walls, floorSegments };
|
|
502
523
|
}
|
|
@@ -1160,6 +1181,9 @@ var OverlayScene = class {
|
|
|
1160
1181
|
if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
|
|
1161
1182
|
wrapHorizontal(entry.body, this.config.bounds);
|
|
1162
1183
|
}
|
|
1184
|
+
if (entry.domElement && entry.tags.includes("falling")) {
|
|
1185
|
+
this.updateDOMElementTransform(entry);
|
|
1186
|
+
}
|
|
1163
1187
|
}
|
|
1164
1188
|
if (!this.config.debug) {
|
|
1165
1189
|
this.drawTTFGlyphs();
|
|
@@ -1185,6 +1209,7 @@ var OverlayScene = class {
|
|
|
1185
1209
|
this.boundaries = [...boundariesResult.walls, ...boundariesResult.floorSegments];
|
|
1186
1210
|
this.floorSegments = boundariesResult.floorSegments;
|
|
1187
1211
|
Matter5.Composite.add(this.engine.world, this.boundaries);
|
|
1212
|
+
this.checkInitialFloorIntegrity();
|
|
1188
1213
|
this.mouse = Matter5.Mouse.create(canvas);
|
|
1189
1214
|
this.mouseConstraint = Matter5.MouseConstraint.create(this.engine, {
|
|
1190
1215
|
mouse: this.mouse,
|
|
@@ -1303,7 +1328,8 @@ var OverlayScene = class {
|
|
|
1303
1328
|
if (onObstacles.has(dyn.id)) continue;
|
|
1304
1329
|
const dynBounds = dyn.body.bounds;
|
|
1305
1330
|
const horizontalOverlap = dynBounds.max.x > segmentBounds.min.x && dynBounds.min.x < segmentBounds.max.x;
|
|
1306
|
-
|
|
1331
|
+
const nearFloor = dynBounds.max.y >= segmentBounds.min.y - 10;
|
|
1332
|
+
if (horizontalOverlap && nearFloor) {
|
|
1307
1333
|
resting.add(dyn.id);
|
|
1308
1334
|
}
|
|
1309
1335
|
}
|
|
@@ -1329,17 +1355,50 @@ var OverlayScene = class {
|
|
|
1329
1355
|
const objectIds = this.floorSegmentPressure.get(i);
|
|
1330
1356
|
const pressure = objectIds ? this.calculateWeightedPressure(objectIds) : 0;
|
|
1331
1357
|
if (pressure >= threshold) {
|
|
1332
|
-
this.collapseFloorSegment(i, pressure
|
|
1358
|
+
this.collapseFloorSegment(i, `pressure ${pressure} >= threshold ${threshold}`);
|
|
1333
1359
|
}
|
|
1334
1360
|
}
|
|
1335
1361
|
}
|
|
1336
1362
|
/** Collapse a single floor segment */
|
|
1337
|
-
collapseFloorSegment(index,
|
|
1363
|
+
collapseFloorSegment(index, reason) {
|
|
1338
1364
|
if (this.collapsedSegments.has(index)) return;
|
|
1339
1365
|
this.collapsedSegments.add(index);
|
|
1340
1366
|
const segment = this.floorSegments[index];
|
|
1341
1367
|
Matter5.Composite.remove(this.engine.world, segment);
|
|
1342
|
-
|
|
1368
|
+
logger.debug("OverlayScene", `Floor segment ${index} collapsed: ${reason}`);
|
|
1369
|
+
this.checkFloorIntegrity();
|
|
1370
|
+
}
|
|
1371
|
+
/** Check if floor integrity requirement is violated and collapse all remaining if so */
|
|
1372
|
+
checkFloorIntegrity() {
|
|
1373
|
+
const minIntegrity = this.config.floorConfig?.minIntegrity;
|
|
1374
|
+
if (minIntegrity === void 0) return;
|
|
1375
|
+
const totalSegments = this.floorSegments.length;
|
|
1376
|
+
const remainingSegments = totalSegments - this.collapsedSegments.size;
|
|
1377
|
+
if (remainingSegments < minIntegrity && remainingSegments > 0) {
|
|
1378
|
+
logger.debug("OverlayScene", `Floor integrity failed: ${remainingSegments} remaining < ${minIntegrity} required. Collapsing all.`);
|
|
1379
|
+
for (let i = 0; i < totalSegments; i++) {
|
|
1380
|
+
if (!this.collapsedSegments.has(i)) {
|
|
1381
|
+
this.collapsedSegments.add(i);
|
|
1382
|
+
const segment = this.floorSegments[i];
|
|
1383
|
+
Matter5.Composite.remove(this.engine.world, segment);
|
|
1384
|
+
logger.debug("OverlayScene", `Floor segment ${i} collapsed: integrity failure cascade`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/** Check floor integrity on initialization (handles minIntegrity > segments) */
|
|
1390
|
+
checkInitialFloorIntegrity() {
|
|
1391
|
+
const minIntegrity = this.config.floorConfig?.minIntegrity;
|
|
1392
|
+
if (minIntegrity === void 0) return;
|
|
1393
|
+
const totalSegments = this.floorSegments.length;
|
|
1394
|
+
if (totalSegments < minIntegrity) {
|
|
1395
|
+
logger.debug("OverlayScene", `Floor integrity impossible: ${totalSegments} segments < ${minIntegrity} required. Collapsing all immediately.`);
|
|
1396
|
+
for (let i = 0; i < totalSegments; i++) {
|
|
1397
|
+
this.collapsedSegments.add(i);
|
|
1398
|
+
const segment = this.floorSegments[i];
|
|
1399
|
+
Matter5.Composite.remove(this.engine.world, segment);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1343
1402
|
}
|
|
1344
1403
|
/** Log a summary of pressure on all obstacles, grouped by word */
|
|
1345
1404
|
logPressureSummary() {
|
|
@@ -1454,6 +1513,15 @@ var OverlayScene = class {
|
|
|
1454
1513
|
if (!entry.originalPosition) return;
|
|
1455
1514
|
const opacity = entry.shadow?.opacity ?? 0.3;
|
|
1456
1515
|
const shadowId = `shadow-${entry.id}`;
|
|
1516
|
+
if (entry.domElement) {
|
|
1517
|
+
const shadowElement = entry.domElement.cloneNode(true);
|
|
1518
|
+
shadowElement.style.opacity = String(opacity);
|
|
1519
|
+
shadowElement.style.pointerEvents = "none";
|
|
1520
|
+
shadowElement.style.transform = entry.domOriginalTransform || "";
|
|
1521
|
+
entry.domElement.parentNode?.insertBefore(shadowElement, entry.domElement);
|
|
1522
|
+
entry.domShadowElement = shadowElement;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1457
1525
|
if (entry.ttfGlyph) {
|
|
1458
1526
|
const body = Matter5.Bodies.circle(entry.originalPosition.x, entry.originalPosition.y, 1, {
|
|
1459
1527
|
isStatic: true,
|
|
@@ -1586,6 +1654,7 @@ var OverlayScene = class {
|
|
|
1586
1654
|
this.collapsedSegments.clear();
|
|
1587
1655
|
this.floorSegmentPressure.clear();
|
|
1588
1656
|
Matter5.Composite.add(this.engine.world, this.boundaries);
|
|
1657
|
+
this.checkInitialFloorIntegrity();
|
|
1589
1658
|
this.render.options.width = width;
|
|
1590
1659
|
this.render.options.height = height;
|
|
1591
1660
|
this.render.canvas.width = width;
|
|
@@ -1602,6 +1671,21 @@ var OverlayScene = class {
|
|
|
1602
1671
|
* Without 'falling' tag, object is static.
|
|
1603
1672
|
*/
|
|
1604
1673
|
spawnObject(config) {
|
|
1674
|
+
if (config.element) {
|
|
1675
|
+
const result = this.addDOMObstacleInternal({
|
|
1676
|
+
element: config.element,
|
|
1677
|
+
x: config.x,
|
|
1678
|
+
y: config.y,
|
|
1679
|
+
width: config.width,
|
|
1680
|
+
height: config.height,
|
|
1681
|
+
tags: config.tags,
|
|
1682
|
+
pressureThreshold: config.pressureThreshold,
|
|
1683
|
+
weight: config.weight,
|
|
1684
|
+
shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
|
|
1685
|
+
clickToFall: config.clickToFall
|
|
1686
|
+
});
|
|
1687
|
+
return result.id;
|
|
1688
|
+
}
|
|
1605
1689
|
const id = crypto.randomUUID();
|
|
1606
1690
|
const tags = config.tags ?? [];
|
|
1607
1691
|
const isStatic = !tags.includes("falling");
|
|
@@ -1621,6 +1705,17 @@ var OverlayScene = class {
|
|
|
1621
1705
|
} else {
|
|
1622
1706
|
body = createObstacle(id, config, isStatic);
|
|
1623
1707
|
}
|
|
1708
|
+
let pressureThreshold;
|
|
1709
|
+
if (config.pressureThreshold) {
|
|
1710
|
+
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
1711
|
+
}
|
|
1712
|
+
let shadow;
|
|
1713
|
+
if (config.shadow === true) {
|
|
1714
|
+
shadow = { opacity: 0.3 };
|
|
1715
|
+
} else if (config.shadow && typeof config.shadow === "object") {
|
|
1716
|
+
shadow = { opacity: config.shadow.opacity ?? 0.3 };
|
|
1717
|
+
}
|
|
1718
|
+
const clicksRemaining = config.clickToFall?.clicks;
|
|
1624
1719
|
const entry = {
|
|
1625
1720
|
id,
|
|
1626
1721
|
body,
|
|
@@ -1628,10 +1723,17 @@ var OverlayScene = class {
|
|
|
1628
1723
|
spawnTime: performance.now(),
|
|
1629
1724
|
ttl: config.ttl,
|
|
1630
1725
|
despawnEffect: config.despawnEffect,
|
|
1631
|
-
weight: config.weight ?? 1
|
|
1726
|
+
weight: config.weight ?? 1,
|
|
1727
|
+
pressureThreshold,
|
|
1728
|
+
shadow,
|
|
1729
|
+
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
1730
|
+
clicksRemaining
|
|
1632
1731
|
};
|
|
1633
1732
|
this.objects.set(id, entry);
|
|
1634
1733
|
Matter5.Composite.add(this.engine.world, body);
|
|
1734
|
+
if (isStatic && pressureThreshold !== void 0) {
|
|
1735
|
+
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
1736
|
+
}
|
|
1635
1737
|
return id;
|
|
1636
1738
|
}
|
|
1637
1739
|
/**
|
|
@@ -1658,6 +1760,17 @@ var OverlayScene = class {
|
|
|
1658
1760
|
} else {
|
|
1659
1761
|
body = await createObstacleAsync(id, config, isStatic);
|
|
1660
1762
|
}
|
|
1763
|
+
let pressureThreshold;
|
|
1764
|
+
if (config.pressureThreshold) {
|
|
1765
|
+
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
1766
|
+
}
|
|
1767
|
+
let shadow;
|
|
1768
|
+
if (config.shadow === true) {
|
|
1769
|
+
shadow = { opacity: 0.3 };
|
|
1770
|
+
} else if (config.shadow && typeof config.shadow === "object") {
|
|
1771
|
+
shadow = { opacity: config.shadow.opacity ?? 0.3 };
|
|
1772
|
+
}
|
|
1773
|
+
const clicksRemaining = config.clickToFall?.clicks;
|
|
1661
1774
|
const entry = {
|
|
1662
1775
|
id,
|
|
1663
1776
|
body,
|
|
@@ -1665,10 +1778,17 @@ var OverlayScene = class {
|
|
|
1665
1778
|
spawnTime: performance.now(),
|
|
1666
1779
|
ttl: config.ttl,
|
|
1667
1780
|
despawnEffect: config.despawnEffect,
|
|
1668
|
-
weight: config.weight ?? 1
|
|
1781
|
+
weight: config.weight ?? 1,
|
|
1782
|
+
pressureThreshold,
|
|
1783
|
+
shadow,
|
|
1784
|
+
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
1785
|
+
clicksRemaining
|
|
1669
1786
|
};
|
|
1670
1787
|
this.objects.set(id, entry);
|
|
1671
1788
|
Matter5.Composite.add(this.engine.world, body);
|
|
1789
|
+
if (isStatic && pressureThreshold !== void 0) {
|
|
1790
|
+
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
1791
|
+
}
|
|
1672
1792
|
return id;
|
|
1673
1793
|
}
|
|
1674
1794
|
/**
|
|
@@ -1927,6 +2047,80 @@ var OverlayScene = class {
|
|
|
1927
2047
|
areFontsInitialized() {
|
|
1928
2048
|
return this.fontsInitialized;
|
|
1929
2049
|
}
|
|
2050
|
+
// ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
|
|
2051
|
+
/**
|
|
2052
|
+
* Internal: Attach a DOM element to physics.
|
|
2053
|
+
* Called by spawnObject when element is provided.
|
|
2054
|
+
*/
|
|
2055
|
+
addDOMObstacleInternal(config) {
|
|
2056
|
+
const { element, x, y } = config;
|
|
2057
|
+
const width = config.width ?? element.offsetWidth;
|
|
2058
|
+
const height = config.height ?? element.offsetHeight;
|
|
2059
|
+
const tags = config.tags ?? [];
|
|
2060
|
+
const isStatic = !tags.includes("falling");
|
|
2061
|
+
const body = Matter5.Bodies.rectangle(x, y, width, height, {
|
|
2062
|
+
isStatic,
|
|
2063
|
+
label: `dom-${crypto.randomUUID().slice(0, 8)}`,
|
|
2064
|
+
render: { visible: false }
|
|
2065
|
+
// Don't render the body, DOM element is the visual
|
|
2066
|
+
});
|
|
2067
|
+
const id = body.label;
|
|
2068
|
+
let pressureThreshold;
|
|
2069
|
+
if (config.pressureThreshold) {
|
|
2070
|
+
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
2071
|
+
}
|
|
2072
|
+
const shadow = config.shadow ? { opacity: config.shadow.opacity ?? 0.3 } : void 0;
|
|
2073
|
+
const clicksRemaining = config.clickToFall?.clicks;
|
|
2074
|
+
const originalTransform = element.style.transform || "";
|
|
2075
|
+
element.style.position = "absolute";
|
|
2076
|
+
element.style.transformOrigin = "center center";
|
|
2077
|
+
const entry = {
|
|
2078
|
+
id,
|
|
2079
|
+
body,
|
|
2080
|
+
tags,
|
|
2081
|
+
spawnTime: performance.now(),
|
|
2082
|
+
pressureThreshold,
|
|
2083
|
+
weight: config.weight ?? 1,
|
|
2084
|
+
shadow,
|
|
2085
|
+
originalPosition: shadow || clicksRemaining !== void 0 ? { x, y } : void 0,
|
|
2086
|
+
clicksRemaining,
|
|
2087
|
+
domElement: element,
|
|
2088
|
+
domOriginalTransform: originalTransform
|
|
2089
|
+
};
|
|
2090
|
+
this.objects.set(id, entry);
|
|
2091
|
+
Matter5.Composite.add(this.engine.world, body);
|
|
2092
|
+
this.updateDOMElementTransform(entry);
|
|
2093
|
+
if (isStatic && pressureThreshold !== void 0) {
|
|
2094
|
+
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2095
|
+
}
|
|
2096
|
+
if (clicksRemaining !== void 0) {
|
|
2097
|
+
const clickHandler = () => {
|
|
2098
|
+
const currentEntry = this.objects.get(id);
|
|
2099
|
+
if (!currentEntry) return;
|
|
2100
|
+
if (currentEntry.tags.includes("falling")) return;
|
|
2101
|
+
if (currentEntry.clicksRemaining === void 0) return;
|
|
2102
|
+
currentEntry.clicksRemaining--;
|
|
2103
|
+
logger.debug("OverlayScene", `Click on DOM element: ${currentEntry.clicksRemaining} clicks remaining`);
|
|
2104
|
+
if (currentEntry.clicksRemaining <= 0) {
|
|
2105
|
+
this.collapseObstacle(currentEntry);
|
|
2106
|
+
element.removeEventListener("click", clickHandler);
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
element.addEventListener("click", clickHandler);
|
|
2110
|
+
}
|
|
2111
|
+
return {
|
|
2112
|
+
id,
|
|
2113
|
+
shadowElement: null
|
|
2114
|
+
// Will be populated on collapse
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Get the shadow element for a DOM obstacle (available after collapse).
|
|
2119
|
+
*/
|
|
2120
|
+
getDOMObstacleShadow(id) {
|
|
2121
|
+
const entry = this.objects.get(id);
|
|
2122
|
+
return entry?.domShadowElement ?? null;
|
|
2123
|
+
}
|
|
1930
2124
|
// ==================== TEXT OBSTACLE METHODS ====================
|
|
1931
2125
|
/**
|
|
1932
2126
|
* Create text obstacles from a string. Each character becomes an individual obstacle
|
|
@@ -2002,11 +2196,61 @@ var OverlayScene = class {
|
|
|
2002
2196
|
maxDimension = Math.max(maxDimension, dims.width, dims.height);
|
|
2003
2197
|
}
|
|
2004
2198
|
if (maxDimension === 0) maxDimension = 100;
|
|
2199
|
+
const calculateLineWidth = (line) => {
|
|
2200
|
+
let width = 0;
|
|
2201
|
+
for (const char of line) {
|
|
2202
|
+
if (char === " ") {
|
|
2203
|
+
width += 20;
|
|
2204
|
+
} else if (/^[A-Za-z0-9]$/.test(char)) {
|
|
2205
|
+
const dims = charDimensions.get(char);
|
|
2206
|
+
if (dims) {
|
|
2207
|
+
const scale = letterSize / Math.max(dims.width, dims.height);
|
|
2208
|
+
const scaledWidth = dims.width * scale;
|
|
2209
|
+
const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
|
|
2210
|
+
width += scaledWidth + Math.max(0, extraSpacing);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return width;
|
|
2215
|
+
};
|
|
2216
|
+
const lineWidths = lines.map((line) => calculateLineWidth(line));
|
|
2217
|
+
const align = config.align ?? "left";
|
|
2218
|
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
|
2219
|
+
let boundsLeft;
|
|
2220
|
+
let boundsRight;
|
|
2221
|
+
switch (align) {
|
|
2222
|
+
case "center":
|
|
2223
|
+
boundsLeft = config.x - maxLineWidth / 2;
|
|
2224
|
+
boundsRight = config.x + maxLineWidth / 2;
|
|
2225
|
+
break;
|
|
2226
|
+
case "right":
|
|
2227
|
+
boundsLeft = config.x - maxLineWidth;
|
|
2228
|
+
boundsRight = config.x;
|
|
2229
|
+
break;
|
|
2230
|
+
default:
|
|
2231
|
+
boundsLeft = config.x;
|
|
2232
|
+
boundsRight = config.x + maxLineWidth;
|
|
2233
|
+
}
|
|
2234
|
+
const boundsTop = config.y - letterSize / 2;
|
|
2235
|
+
const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
|
|
2236
|
+
const boundsBottom = boundsTop + totalHeight;
|
|
2005
2237
|
let currentY = config.y;
|
|
2006
2238
|
let globalCharIndex = 0;
|
|
2007
|
-
for (
|
|
2239
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2240
|
+
const line = lines[lineIndex];
|
|
2008
2241
|
const chars = line.split("");
|
|
2009
|
-
|
|
2242
|
+
const lineWidth = lineWidths[lineIndex];
|
|
2243
|
+
let currentX;
|
|
2244
|
+
switch (align) {
|
|
2245
|
+
case "center":
|
|
2246
|
+
currentX = config.x - lineWidth / 2;
|
|
2247
|
+
break;
|
|
2248
|
+
case "right":
|
|
2249
|
+
currentX = config.x - lineWidth;
|
|
2250
|
+
break;
|
|
2251
|
+
default:
|
|
2252
|
+
currentX = config.x;
|
|
2253
|
+
}
|
|
2010
2254
|
if (inWord) {
|
|
2011
2255
|
currentWordIndex++;
|
|
2012
2256
|
inWord = false;
|
|
@@ -2122,12 +2366,21 @@ var OverlayScene = class {
|
|
|
2122
2366
|
letterColor,
|
|
2123
2367
|
lineCount: lines.length
|
|
2124
2368
|
});
|
|
2369
|
+
const bounds = {
|
|
2370
|
+
left: boundsLeft,
|
|
2371
|
+
right: boundsRight,
|
|
2372
|
+
top: boundsTop,
|
|
2373
|
+
bottom: boundsBottom,
|
|
2374
|
+
width: boundsRight - boundsLeft,
|
|
2375
|
+
height: boundsBottom - boundsTop
|
|
2376
|
+
};
|
|
2125
2377
|
return {
|
|
2126
2378
|
letterIds,
|
|
2127
2379
|
stringTag,
|
|
2128
2380
|
wordTags,
|
|
2129
2381
|
letterMap,
|
|
2130
|
-
letterDebugInfo: debugInfo
|
|
2382
|
+
letterDebugInfo: debugInfo,
|
|
2383
|
+
bounds
|
|
2131
2384
|
};
|
|
2132
2385
|
}
|
|
2133
2386
|
/**
|
|
@@ -2200,10 +2453,43 @@ var OverlayScene = class {
|
|
|
2200
2453
|
const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
|
|
2201
2454
|
const fontFamily = fontInfo?.name ?? "sans-serif";
|
|
2202
2455
|
const lines = text.split("\n");
|
|
2456
|
+
const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
|
|
2457
|
+
const align = config.align ?? "left";
|
|
2458
|
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
|
2459
|
+
let boundsLeft;
|
|
2460
|
+
let boundsRight;
|
|
2461
|
+
switch (align) {
|
|
2462
|
+
case "center":
|
|
2463
|
+
boundsLeft = x - maxLineWidth / 2;
|
|
2464
|
+
boundsRight = x + maxLineWidth / 2;
|
|
2465
|
+
break;
|
|
2466
|
+
case "right":
|
|
2467
|
+
boundsLeft = x - maxLineWidth;
|
|
2468
|
+
boundsRight = x;
|
|
2469
|
+
break;
|
|
2470
|
+
default:
|
|
2471
|
+
boundsLeft = x;
|
|
2472
|
+
boundsRight = x + maxLineWidth;
|
|
2473
|
+
}
|
|
2474
|
+
const boundsTop = y - fontSize * 0.8;
|
|
2475
|
+
const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
|
|
2476
|
+
const boundsBottom = boundsTop + totalHeight;
|
|
2203
2477
|
let currentY = y;
|
|
2204
2478
|
let globalCharIndex = 0;
|
|
2205
|
-
for (
|
|
2206
|
-
|
|
2479
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2480
|
+
const line = lines[lineIndex];
|
|
2481
|
+
const lineWidth = lineWidths[lineIndex];
|
|
2482
|
+
let currentX;
|
|
2483
|
+
switch (align) {
|
|
2484
|
+
case "center":
|
|
2485
|
+
currentX = x - lineWidth / 2;
|
|
2486
|
+
break;
|
|
2487
|
+
case "right":
|
|
2488
|
+
currentX = x - lineWidth;
|
|
2489
|
+
break;
|
|
2490
|
+
default:
|
|
2491
|
+
currentX = x;
|
|
2492
|
+
}
|
|
2207
2493
|
if (inWord) {
|
|
2208
2494
|
currentWordIndex++;
|
|
2209
2495
|
inWord = false;
|
|
@@ -2313,12 +2599,21 @@ var OverlayScene = class {
|
|
|
2313
2599
|
wordTags,
|
|
2314
2600
|
lineCount: lines.length
|
|
2315
2601
|
});
|
|
2602
|
+
const bounds = {
|
|
2603
|
+
left: boundsLeft,
|
|
2604
|
+
right: boundsRight,
|
|
2605
|
+
top: boundsTop,
|
|
2606
|
+
bottom: boundsBottom,
|
|
2607
|
+
width: boundsRight - boundsLeft,
|
|
2608
|
+
height: boundsBottom - boundsTop
|
|
2609
|
+
};
|
|
2316
2610
|
return {
|
|
2317
2611
|
letterIds,
|
|
2318
2612
|
stringTag,
|
|
2319
2613
|
wordTags,
|
|
2320
2614
|
letterMap,
|
|
2321
|
-
letterDebugInfo: []
|
|
2615
|
+
letterDebugInfo: [],
|
|
2616
|
+
bounds
|
|
2322
2617
|
};
|
|
2323
2618
|
}
|
|
2324
2619
|
/**
|
|
@@ -2423,6 +2718,22 @@ var OverlayScene = class {
|
|
|
2423
2718
|
ctx.restore();
|
|
2424
2719
|
}
|
|
2425
2720
|
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Update a DOM element's CSS transform to match its physics body position and rotation.
|
|
2723
|
+
*/
|
|
2724
|
+
updateDOMElementTransform(entry) {
|
|
2725
|
+
if (!entry.domElement) return;
|
|
2726
|
+
const body = entry.body;
|
|
2727
|
+
const x = body.position.x;
|
|
2728
|
+
const y = body.position.y;
|
|
2729
|
+
const angle = body.angle;
|
|
2730
|
+
const angleDeg = angle * (180 / Math.PI);
|
|
2731
|
+
const width = entry.domElement.offsetWidth;
|
|
2732
|
+
const height = entry.domElement.offsetHeight;
|
|
2733
|
+
entry.domElement.style.left = `${x - width / 2}px`;
|
|
2734
|
+
entry.domElement.style.top = `${y - height / 2}px`;
|
|
2735
|
+
entry.domElement.style.transform = `rotate(${angleDeg}deg)`;
|
|
2736
|
+
}
|
|
2426
2737
|
checkTTLExpiration() {
|
|
2427
2738
|
const now = performance.now();
|
|
2428
2739
|
const expiredObjects = [];
|