@blorkfield/overlay-core 0.7.1 → 0.8.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/dist/index.cjs +253 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -3
- package/dist/index.d.ts +138 -3
- package/dist/index.js +253 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -854,12 +854,6 @@ function clearFontCache() {
|
|
|
854
854
|
|
|
855
855
|
// src/entity.ts
|
|
856
856
|
import Matter3 from "matter-js";
|
|
857
|
-
var MOUSE_FORCE = 1e-3;
|
|
858
|
-
function applyMouseForce(entity, mouseX, grounded) {
|
|
859
|
-
if (!grounded) return;
|
|
860
|
-
const direction = Math.sign(mouseX - entity.position.x);
|
|
861
|
-
Matter3.Body.applyForce(entity, entity.position, { x: MOUSE_FORCE * direction, y: 0 });
|
|
862
|
-
}
|
|
863
857
|
function wrapHorizontal(entity, bounds) {
|
|
864
858
|
if (entity.position.x < bounds.left) {
|
|
865
859
|
Matter3.Body.setPosition(entity, { x: bounds.right, y: entity.position.y });
|
|
@@ -1380,7 +1374,6 @@ var OverlayScene = class {
|
|
|
1380
1374
|
this.objects = /* @__PURE__ */ new Map();
|
|
1381
1375
|
this.boundaries = [];
|
|
1382
1376
|
this.updateCallbacks = [];
|
|
1383
|
-
this.mouseX = 0;
|
|
1384
1377
|
this.animationFrameId = null;
|
|
1385
1378
|
this.mouse = null;
|
|
1386
1379
|
this.mouseConstraint = null;
|
|
@@ -1397,6 +1390,14 @@ var OverlayScene = class {
|
|
|
1397
1390
|
this.floorSegmentPressure = /* @__PURE__ */ new Map();
|
|
1398
1391
|
// segment index -> object IDs
|
|
1399
1392
|
this.collapsedSegments = /* @__PURE__ */ new Set();
|
|
1393
|
+
// Lifecycle event callbacks
|
|
1394
|
+
this.lifecycleCallbacks = {
|
|
1395
|
+
objectSpawned: [],
|
|
1396
|
+
objectRemoved: [],
|
|
1397
|
+
objectCollision: []
|
|
1398
|
+
};
|
|
1399
|
+
// Follow targets for follow-{key} tagged objects
|
|
1400
|
+
this.followTargets = /* @__PURE__ */ new Map();
|
|
1400
1401
|
/** Filter drag events - only allow grabbing objects with 'grabable' tag */
|
|
1401
1402
|
this.handleStartDrag = (event) => {
|
|
1402
1403
|
const body = event.body;
|
|
@@ -1446,17 +1447,48 @@ var OverlayScene = class {
|
|
|
1446
1447
|
this.handleAfterRender = () => {
|
|
1447
1448
|
this.backgroundManager.renderOverlay();
|
|
1448
1449
|
};
|
|
1450
|
+
/**
|
|
1451
|
+
* Handler for Matter.js collision events.
|
|
1452
|
+
* Emits objectCollision lifecycle events.
|
|
1453
|
+
*/
|
|
1454
|
+
this.handleCollisionStart = (event) => {
|
|
1455
|
+
for (const pair of event.pairs) {
|
|
1456
|
+
const entryA = this.findObjectByBody(pair.bodyA);
|
|
1457
|
+
const entryB = this.findObjectByBody(pair.bodyB);
|
|
1458
|
+
if (entryA && entryB) {
|
|
1459
|
+
this.emitLifecycleEvent(
|
|
1460
|
+
"objectCollision",
|
|
1461
|
+
this.toObjectState(entryA),
|
|
1462
|
+
this.toObjectState(entryB)
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1449
1467
|
// ==================== PRIVATE ====================
|
|
1450
1468
|
this.loop = () => {
|
|
1451
1469
|
this.effectManager.update();
|
|
1452
1470
|
this.checkTTLExpiration();
|
|
1453
1471
|
this.checkDespawnBelowFloor();
|
|
1454
1472
|
this.updatePressure();
|
|
1455
|
-
|
|
1473
|
+
if (!this.followTargets.has("mouse") && this.mouse) {
|
|
1474
|
+
this.followTargets.set("mouse", { x: this.mouse.position.x, y: this.mouse.position.y });
|
|
1475
|
+
}
|
|
1456
1476
|
for (const entry of this.objects.values()) {
|
|
1457
1477
|
const isDragging = this.mouseConstraint?.body === entry.body;
|
|
1458
|
-
if (!isDragging
|
|
1459
|
-
|
|
1478
|
+
if (!isDragging) {
|
|
1479
|
+
for (const tag of entry.tags) {
|
|
1480
|
+
const key = tag === "follow" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
|
|
1481
|
+
if (key) {
|
|
1482
|
+
const target = this.followTargets.get(key);
|
|
1483
|
+
if (target) {
|
|
1484
|
+
const grounded = this.isGrounded(entry.body);
|
|
1485
|
+
if (grounded) {
|
|
1486
|
+
const direction = Math.sign(target.x - entry.body.position.x);
|
|
1487
|
+
Matter5.Body.applyForce(entry.body, entry.body.position, { x: 1e-3 * direction, y: 0 });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1460
1492
|
}
|
|
1461
1493
|
if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
|
|
1462
1494
|
wrapHorizontal(entry.body, this.config.bounds);
|
|
@@ -1530,6 +1562,7 @@ var OverlayScene = class {
|
|
|
1530
1562
|
}
|
|
1531
1563
|
Matter5.Events.on(this.render, "beforeRender", this.handleBeforeRender);
|
|
1532
1564
|
Matter5.Events.on(this.render, "afterRender", this.handleAfterRender);
|
|
1565
|
+
Matter5.Events.on(this.engine, "collisionStart", this.handleCollisionStart);
|
|
1533
1566
|
}
|
|
1534
1567
|
static createContainer(parent, options = {}) {
|
|
1535
1568
|
const canvas = document.createElement("canvas");
|
|
@@ -1934,6 +1967,7 @@ var OverlayScene = class {
|
|
|
1934
1967
|
this.canvas.removeEventListener("click", this.handleCanvasClick);
|
|
1935
1968
|
Matter5.Events.off(this.render, "beforeRender", this.handleBeforeRender);
|
|
1936
1969
|
Matter5.Events.off(this.render, "afterRender", this.handleAfterRender);
|
|
1970
|
+
Matter5.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
|
|
1937
1971
|
Matter5.Engine.clear(this.engine);
|
|
1938
1972
|
this.objects.clear();
|
|
1939
1973
|
this.obstaclePressure.clear();
|
|
@@ -1942,6 +1976,10 @@ var OverlayScene = class {
|
|
|
1942
1976
|
this.floorSegmentPressure.clear();
|
|
1943
1977
|
this.collapsedSegments.clear();
|
|
1944
1978
|
this.updateCallbacks = [];
|
|
1979
|
+
this.lifecycleCallbacks.objectSpawned = [];
|
|
1980
|
+
this.lifecycleCallbacks.objectRemoved = [];
|
|
1981
|
+
this.lifecycleCallbacks.objectCollision = [];
|
|
1982
|
+
this.followTargets.clear();
|
|
1945
1983
|
}
|
|
1946
1984
|
setDebug(enabled) {
|
|
1947
1985
|
this.config.debug = enabled;
|
|
@@ -2056,6 +2094,7 @@ var OverlayScene = class {
|
|
|
2056
2094
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2057
2095
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2058
2096
|
}
|
|
2097
|
+
this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
|
|
2059
2098
|
return id;
|
|
2060
2099
|
}
|
|
2061
2100
|
/**
|
|
@@ -2111,6 +2150,7 @@ var OverlayScene = class {
|
|
|
2111
2150
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2112
2151
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2113
2152
|
}
|
|
2153
|
+
this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
|
|
2114
2154
|
return id;
|
|
2115
2155
|
}
|
|
2116
2156
|
/**
|
|
@@ -2192,6 +2232,7 @@ var OverlayScene = class {
|
|
|
2192
2232
|
removeObject(id) {
|
|
2193
2233
|
const entry = this.objects.get(id);
|
|
2194
2234
|
if (!entry) return;
|
|
2235
|
+
this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
|
|
2195
2236
|
Matter5.Composite.remove(this.engine.world, entry.body);
|
|
2196
2237
|
this.objects.delete(id);
|
|
2197
2238
|
}
|
|
@@ -2241,8 +2282,208 @@ var OverlayScene = class {
|
|
|
2241
2282
|
}
|
|
2242
2283
|
return Array.from(tagsSet).sort();
|
|
2243
2284
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2285
|
+
/**
|
|
2286
|
+
* Set the mouse position for follow behavior.
|
|
2287
|
+
* This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
|
|
2288
|
+
* @deprecated Use setFollowTarget('mouse', x, y) instead
|
|
2289
|
+
*/
|
|
2290
|
+
setMousePosition(x, y) {
|
|
2291
|
+
this.setFollowTarget("mouse", x, y);
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Set a follow target position. Objects with 'follow-{key}' tag will
|
|
2295
|
+
* automatically move toward this target each frame.
|
|
2296
|
+
* @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
|
|
2297
|
+
* @param x - Target X position
|
|
2298
|
+
* @param y - Target Y position
|
|
2299
|
+
*/
|
|
2300
|
+
setFollowTarget(key, x, y) {
|
|
2301
|
+
this.followTargets.set(key, { x, y });
|
|
2302
|
+
if (key === "mouse" && this.mouse) {
|
|
2303
|
+
this.mouse.position.x = x;
|
|
2304
|
+
this.mouse.position.y = y;
|
|
2305
|
+
this.mouse.absolute.x = x;
|
|
2306
|
+
this.mouse.absolute.y = y;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Remove a follow target. Objects with the corresponding tag will stop following.
|
|
2311
|
+
* @param key - The target key to remove
|
|
2312
|
+
*/
|
|
2313
|
+
removeFollowTarget(key) {
|
|
2314
|
+
this.followTargets.delete(key);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Get all registered follow target keys.
|
|
2318
|
+
* @returns Array of follow target keys
|
|
2319
|
+
*/
|
|
2320
|
+
getFollowTargetKeys() {
|
|
2321
|
+
return Array.from(this.followTargets.keys());
|
|
2322
|
+
}
|
|
2323
|
+
// ==================== GRAB/DRAG METHODS ====================
|
|
2324
|
+
/**
|
|
2325
|
+
* Programmatically grab an object at the current mouse position.
|
|
2326
|
+
* Uses the externally set mouse position (via setFollowTarget('mouse', x, y))
|
|
2327
|
+
* or the native canvas mouse position if no external position is set.
|
|
2328
|
+
* Only objects with the 'grabable' tag can be grabbed.
|
|
2329
|
+
* @returns The ID of the grabbed object, or null if no grabable object at position
|
|
2330
|
+
*/
|
|
2331
|
+
startGrab() {
|
|
2332
|
+
if (!this.mouseConstraint || !this.mouse) return null;
|
|
2333
|
+
const mouseTarget = this.followTargets.get("mouse");
|
|
2334
|
+
const position = mouseTarget ?? { x: this.mouse.position.x, y: this.mouse.position.y };
|
|
2335
|
+
const bodies = Matter5.Query.point(
|
|
2336
|
+
Matter5.Composite.allBodies(this.engine.world),
|
|
2337
|
+
position
|
|
2338
|
+
);
|
|
2339
|
+
for (const body of bodies) {
|
|
2340
|
+
const entry = this.findObjectByBody(body);
|
|
2341
|
+
if (entry && entry.tags.includes("grabable")) {
|
|
2342
|
+
this.mouseConstraint.constraint.bodyB = entry.body;
|
|
2343
|
+
this.mouseConstraint.constraint.pointB = {
|
|
2344
|
+
x: position.x - entry.body.position.x,
|
|
2345
|
+
y: position.y - entry.body.position.y
|
|
2346
|
+
};
|
|
2347
|
+
return entry.id;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Release any currently grabbed object.
|
|
2354
|
+
*/
|
|
2355
|
+
endGrab() {
|
|
2356
|
+
if (this.mouseConstraint) {
|
|
2357
|
+
this.mouseConstraint.constraint.bodyB = null;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Get the ID of the currently grabbed object.
|
|
2362
|
+
* @returns The ID of the grabbed object, or null if nothing is grabbed
|
|
2363
|
+
*/
|
|
2364
|
+
getGrabbedObject() {
|
|
2365
|
+
if (!this.mouseConstraint?.constraint.bodyB) return null;
|
|
2366
|
+
const entry = this.findObjectByBody(this.mouseConstraint.constraint.bodyB);
|
|
2367
|
+
return entry?.id ?? null;
|
|
2368
|
+
}
|
|
2369
|
+
// ==================== PHYSICS MANIPULATION METHODS ====================
|
|
2370
|
+
/**
|
|
2371
|
+
* Apply a force to an object.
|
|
2372
|
+
* @param objectId - The ID of the object
|
|
2373
|
+
* @param force - The force vector to apply
|
|
2374
|
+
*/
|
|
2375
|
+
applyForce(objectId, force) {
|
|
2376
|
+
const entry = this.objects.get(objectId);
|
|
2377
|
+
if (!entry) return;
|
|
2378
|
+
Matter5.Body.applyForce(entry.body, entry.body.position, force);
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Apply a force to all objects with a specific tag.
|
|
2382
|
+
* @param tag - The tag to match
|
|
2383
|
+
* @param force - The force vector to apply
|
|
2384
|
+
*/
|
|
2385
|
+
applyForceToTag(tag, force) {
|
|
2386
|
+
for (const entry of this.objects.values()) {
|
|
2387
|
+
if (entry.tags.includes(tag)) {
|
|
2388
|
+
Matter5.Body.applyForce(entry.body, entry.body.position, force);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Set the velocity of an object.
|
|
2394
|
+
* @param objectId - The ID of the object
|
|
2395
|
+
* @param velocity - The velocity vector to set
|
|
2396
|
+
*/
|
|
2397
|
+
setVelocity(objectId, velocity) {
|
|
2398
|
+
const entry = this.objects.get(objectId);
|
|
2399
|
+
if (!entry) return;
|
|
2400
|
+
Matter5.Body.setVelocity(entry.body, velocity);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Set the position of an object.
|
|
2404
|
+
* @param objectId - The ID of the object
|
|
2405
|
+
* @param position - The position to set
|
|
2406
|
+
*/
|
|
2407
|
+
setPosition(objectId, position) {
|
|
2408
|
+
const entry = this.objects.get(objectId);
|
|
2409
|
+
if (!entry) return;
|
|
2410
|
+
Matter5.Body.setPosition(entry.body, position);
|
|
2411
|
+
}
|
|
2412
|
+
// ==================== OBJECT STATE METHODS ====================
|
|
2413
|
+
/**
|
|
2414
|
+
* Get the current state of an object.
|
|
2415
|
+
* @param id - The ID of the object
|
|
2416
|
+
* @returns The object state, or null if not found
|
|
2417
|
+
*/
|
|
2418
|
+
getObject(id) {
|
|
2419
|
+
const entry = this.objects.get(id);
|
|
2420
|
+
if (!entry) return null;
|
|
2421
|
+
return {
|
|
2422
|
+
id: entry.id,
|
|
2423
|
+
x: entry.body.position.x,
|
|
2424
|
+
y: entry.body.position.y,
|
|
2425
|
+
velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
|
|
2426
|
+
angle: entry.body.angle,
|
|
2427
|
+
tags: [...entry.tags]
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Get the current state of all objects with a specific tag.
|
|
2432
|
+
* @param tag - The tag to match
|
|
2433
|
+
* @returns Array of object states
|
|
2434
|
+
*/
|
|
2435
|
+
getObjectsByTag(tag) {
|
|
2436
|
+
const result = [];
|
|
2437
|
+
for (const entry of this.objects.values()) {
|
|
2438
|
+
if (entry.tags.includes(tag)) {
|
|
2439
|
+
result.push({
|
|
2440
|
+
id: entry.id,
|
|
2441
|
+
x: entry.body.position.x,
|
|
2442
|
+
y: entry.body.position.y,
|
|
2443
|
+
velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
|
|
2444
|
+
angle: entry.body.angle,
|
|
2445
|
+
tags: [...entry.tags]
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return result;
|
|
2450
|
+
}
|
|
2451
|
+
// ==================== LIFECYCLE EVENTS ====================
|
|
2452
|
+
/**
|
|
2453
|
+
* Subscribe to a lifecycle event.
|
|
2454
|
+
* @param event - The event type to subscribe to
|
|
2455
|
+
* @param callback - The callback to invoke when the event occurs
|
|
2456
|
+
*/
|
|
2457
|
+
on(event, callback) {
|
|
2458
|
+
this.lifecycleCallbacks[event].push(callback);
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Unsubscribe from a lifecycle event.
|
|
2462
|
+
* @param event - The event type to unsubscribe from
|
|
2463
|
+
* @param callback - The callback to remove
|
|
2464
|
+
*/
|
|
2465
|
+
off(event, callback) {
|
|
2466
|
+
const arr = this.lifecycleCallbacks[event];
|
|
2467
|
+
const idx = arr.indexOf(callback);
|
|
2468
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
2469
|
+
}
|
|
2470
|
+
/** Create ObjectState from an ObjectEntry */
|
|
2471
|
+
toObjectState(entry) {
|
|
2472
|
+
return {
|
|
2473
|
+
id: entry.id,
|
|
2474
|
+
x: entry.body.position.x,
|
|
2475
|
+
y: entry.body.position.y,
|
|
2476
|
+
velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
|
|
2477
|
+
angle: entry.body.angle,
|
|
2478
|
+
tags: [...entry.tags]
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
/** Emit a lifecycle event to all registered callbacks */
|
|
2482
|
+
emitLifecycleEvent(event, ...args) {
|
|
2483
|
+
const callbacks = this.lifecycleCallbacks[event];
|
|
2484
|
+
for (const cb of callbacks) {
|
|
2485
|
+
cb(...args);
|
|
2486
|
+
}
|
|
2246
2487
|
}
|
|
2247
2488
|
// ==================== PRESSURE TRACKING METHODS ====================
|
|
2248
2489
|
/**
|