@blorkfield/overlay-core 0.8.10 → 0.9.0

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
@@ -40,6 +40,7 @@ scene.spawnObject({ tags: [FALLING, GRABABLE], ... });
40
40
  | `TAG_FALLING` / `TAGS.FALLING` | `'falling'` | Object is dynamic and affected by gravity |
41
41
  | `TAG_FOLLOW_WINDOW` / `TAGS.FOLLOW_WINDOW` | `'follow_window'` | Object follows mouse position when grounded |
42
42
  | `TAG_GRABABLE` / `TAGS.GRABABLE` | `'grabable'` | Object can be grabbed and moved with mouse |
43
+ | `TAG_GRAVITY_OVERRIDE` / `TAGS.GRAVITY_OVERRIDE` | `'gravity_override'` | Object uses its own gravity (set via `gravityOverride` in config) |
43
44
 
44
45
  Without the `falling` tag, objects are static and won't move.
45
46
 
@@ -82,7 +83,7 @@ const { canvas, bounds } = OverlayScene.createContainer(container, {
82
83
  // Create scene
83
84
  const scene = new OverlayScene(canvas, {
84
85
  bounds,
85
- gravity: 1,
86
+ gravity: { x: 0, y: 1 },
86
87
  wrapHorizontal: true,
87
88
  background: 'transparent'
88
89
  });
@@ -426,7 +427,7 @@ const currentGrab = scene.getGrabbedObject(); // Returns ID or null
426
427
  ```typescript
427
428
  const scene = new OverlayScene(canvas, {
428
429
  bounds: { top: 0, bottom: 600, left: 0, right: 800 },
429
- gravity: 1,
430
+ gravity: { x: 0, y: 1 },
430
431
  wrapHorizontal: true,
431
432
  debug: false,
432
433
  background: '#16213e',
@@ -443,7 +444,7 @@ const scene = new OverlayScene(canvas, {
443
444
 
444
445
  | Option | Default | Description |
445
446
  |--------|---------|-------------|
446
- | `gravity` | 1 | Gravity strength |
447
+ | `gravity` | `{ x: 0, y: 1 }` | Gravity vector. Both axes support negative values |
447
448
  | `wrapHorizontal` | true | Objects wrap around screen edges |
448
449
  | `debug` | false | Show collision wireframes |
449
450
  | `background` | transparent | Canvas background color |
@@ -641,13 +642,53 @@ setLogLevel('debug'); // Options: debug, info, warn, error
641
642
  ## Lifecycle
642
643
 
643
644
  ```typescript
644
- scene.start(); // Start simulation
645
- scene.stop(); // Pause simulation
646
- scene.resize(w, h); // Resize canvas and bounds
647
- scene.setDebug(true); // Toggle wireframe mode
648
- scene.destroy(); // Clean up resources
645
+ scene.start(); // Start simulation
646
+ scene.stop(); // Pause simulation
647
+ scene.resize(w, h); // Resize canvas and bounds
648
+ scene.setDebug(true); // Toggle wireframe mode
649
+ scene.setGravity({ x: 0, y: -1 }); // Set gravity (negative y = upward)
650
+ scene.setGravity({ x: 0, y: 0 }); // Zero gravity
651
+ scene.setGravity({ x: 1, y: 0 }); // Sideways gravity
652
+ scene.destroy(); // Clean up resources
649
653
  ```
650
654
 
655
+ ### Per-Object Gravity Override
656
+
657
+ Individual dynamic objects can have their own gravity vector, independent of the scene gravity. This is done via the `gravityOverride` field in `ObjectConfig`, which automatically adds the `gravity_override` tag to the object.
658
+
659
+ ```typescript
660
+ // Spawn a floaty object that drifts upward
661
+ scene.spawnObject({
662
+ x: 200, y: 300,
663
+ radius: 20,
664
+ fillStyle: '#4a90d9',
665
+ tags: ['falling', 'grabable'],
666
+ gravityOverride: { x: 0, y: -0.3 } // floats upward
667
+ });
668
+
669
+ // Zero gravity — hovers in place
670
+ scene.spawnObject({
671
+ x: 400, y: 200,
672
+ radius: 15,
673
+ fillStyle: '#e94560',
674
+ tags: ['falling'],
675
+ gravityOverride: { x: 0, y: 0 }
676
+ });
677
+
678
+ // Change or clear a gravity override at runtime
679
+ scene.setObjectGravityOverride(id, { x: 0.5, y: 0 }); // drift sideways
680
+ scene.setObjectGravityOverride(id, null); // restore scene gravity
681
+ ```
682
+
683
+ Tags are either boolean (presence = true) or carry a value. `gravity_override` is a value tag — its Vector2 value is set via `gravityOverride` in the config. Boolean tags (`falling`, `grabable`, `follow_window`) need no value.
684
+
685
+ | Tag | Type | Behavior |
686
+ |-----|------|----------|
687
+ | `falling` | boolean | Dynamic body affected by gravity |
688
+ | `grabable` | boolean | Can be grabbed with mouse |
689
+ | `follow_window` | boolean | Walks toward mouse when grounded |
690
+ | `gravity_override` | Vector2 | Uses own gravity instead of scene gravity |
691
+
651
692
  ## Examples
652
693
 
653
694
  Working examples are provided in the `/examples` directory:
@@ -673,6 +714,7 @@ import type {
673
714
  // Scene configuration
674
715
  OverlaySceneConfig,
675
716
  Bounds,
717
+ Vector2,
676
718
  ContainerOptions,
677
719
  FloorConfig,
678
720
 
@@ -732,5 +774,5 @@ import type {
732
774
  } from '@blorkfield/overlay-core';
733
775
 
734
776
  // Tag constants (values, not types)
735
- import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
777
+ import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW, TAG_GRAVITY_OVERRIDE } from '@blorkfield/overlay-core';
736
778
  ```
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  TAG_FALLING: () => TAG_FALLING,
37
37
  TAG_FOLLOW_WINDOW: () => TAG_FOLLOW_WINDOW,
38
38
  TAG_GRABABLE: () => TAG_GRABABLE,
39
+ TAG_GRAVITY_OVERRIDE: () => TAG_GRAVITY_OVERRIDE,
39
40
  clearFontCache: () => clearFontCache,
40
41
  getGlyphData: () => getGlyphData,
41
42
  getKerning: () => getKerning,
@@ -54,7 +55,8 @@ var import_matter_js5 = __toESM(require("matter-js"), 1);
54
55
  var import_matter_js = __toESM(require("matter-js"), 1);
55
56
  function createEngine(gravity) {
56
57
  const engine = import_matter_js.default.Engine.create();
57
- engine.gravity.y = gravity;
58
+ engine.gravity.x = gravity.x;
59
+ engine.gravity.y = gravity.y;
58
60
  return engine;
59
61
  }
60
62
  function createRender(engine, canvas, config) {
@@ -1424,7 +1426,6 @@ var OverlayScene = class {
1424
1426
  this.boundaries = [];
1425
1427
  this.updateCallbacks = [];
1426
1428
  this.animationFrameId = null;
1427
- this.mouse = null;
1428
1429
  this.fonts = [];
1429
1430
  this.fontsInitialized = false;
1430
1431
  this.letterDebugInfo = /* @__PURE__ */ new Map();
@@ -1457,6 +1458,8 @@ var OverlayScene = class {
1457
1458
  this.grabHistoryRadius = 20;
1458
1459
  // Number of physics substeps per frame — more substeps = better collision at high speeds, more CPU
1459
1460
  this.substeps = 2;
1461
+ // Tracks only the bodies with a gravity override — engine gravity handles everyone else
1462
+ this.gravityOverrideEntries = /* @__PURE__ */ new Set();
1460
1463
  /** Handle mouse down - start grab via programmatic API */
1461
1464
  this.handleMouseDown = (event) => {
1462
1465
  const rect = this.canvas.getBoundingClientRect();
@@ -1534,16 +1537,22 @@ var OverlayScene = class {
1534
1537
  // ==================== PRIVATE ====================
1535
1538
  this.loop = () => {
1536
1539
  const substepDelta = 1e3 / 60 / this.substeps;
1540
+ const scale = this.engine.gravity.scale;
1537
1541
  for (let i = 0; i < this.substeps; i++) {
1542
+ for (const entry of this.gravityOverrideEntries) {
1543
+ if (entry.body.isStatic || entry.body.isSleeping) continue;
1544
+ const g = entry.gravityOverride;
1545
+ import_matter_js5.default.Body.applyForce(entry.body, entry.body.position, {
1546
+ x: entry.body.mass * (g.x - this.engine.gravity.x) * scale,
1547
+ y: entry.body.mass * (g.y - this.engine.gravity.y) * scale
1548
+ });
1549
+ }
1538
1550
  import_matter_js5.default.Engine.update(this.engine, substepDelta);
1539
1551
  }
1540
1552
  this.effectManager.update();
1541
1553
  this.checkTTLExpiration();
1542
1554
  this.checkDespawnBelowFloor();
1543
1555
  this.updatePressure();
1544
- if (!this.followTargets.has("mouse") && this.mouse) {
1545
- this.followTargets.set("mouse", { x: this.mouse.position.x, y: this.mouse.position.y });
1546
- }
1547
1556
  if (this.grabbedObjectId && this.lastGrabMousePosition) {
1548
1557
  const entry = this.objects.get(this.grabbedObjectId);
1549
1558
  const mouseTarget = this.followTargets.get("mouse");
@@ -1600,7 +1609,7 @@ var OverlayScene = class {
1600
1609
  };
1601
1610
  this.canvas = canvas;
1602
1611
  this.config = {
1603
- gravity: 1,
1612
+ gravity: { x: 0, y: 1 },
1604
1613
  wrapHorizontal: true,
1605
1614
  debug: false,
1606
1615
  ...config
@@ -1613,7 +1622,6 @@ var OverlayScene = class {
1613
1622
  this.floorSegments = boundariesResult.floorSegments;
1614
1623
  import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
1615
1624
  this.checkInitialFloorIntegrity();
1616
- this.mouse = import_matter_js5.default.Mouse.create(canvas);
1617
1625
  canvas.addEventListener("mousedown", this.handleMouseDown);
1618
1626
  canvas.addEventListener("mousemove", this.handleMouseMove);
1619
1627
  canvas.addEventListener("mouseup", this.handleMouseUp);
@@ -2067,6 +2075,39 @@ var OverlayScene = class {
2067
2075
  }
2068
2076
  }
2069
2077
  }
2078
+ /**
2079
+ * Set gravity at runtime. Supports any direction including negative values.
2080
+ * @example
2081
+ * scene.setGravity({ x: 0, y: 1 }); // Normal downward gravity
2082
+ * scene.setGravity({ x: 0, y: -1 }); // Upward gravity
2083
+ * scene.setGravity({ x: 1, y: 0 }); // Sideways gravity
2084
+ * scene.setGravity({ x: 0, y: 0 }); // Zero gravity
2085
+ */
2086
+ setGravity(gravity) {
2087
+ this.config.gravity = gravity;
2088
+ this.engine.gravity.x = gravity.x;
2089
+ this.engine.gravity.y = gravity.y;
2090
+ }
2091
+ /**
2092
+ * Set or clear a per-object gravity override at runtime.
2093
+ * Pass `null` to remove the override and restore scene gravity for that object.
2094
+ */
2095
+ setObjectGravityOverride(id, gravity) {
2096
+ const entry = this.objects.get(id);
2097
+ if (!entry) return;
2098
+ if (gravity === null) {
2099
+ entry.gravityOverride = void 0;
2100
+ this.gravityOverrideEntries.delete(entry);
2101
+ const idx = entry.tags.indexOf("gravity_override");
2102
+ if (idx !== -1) entry.tags.splice(idx, 1);
2103
+ } else {
2104
+ entry.gravityOverride = gravity;
2105
+ this.gravityOverrideEntries.add(entry);
2106
+ if (!entry.tags.includes("gravity_override")) {
2107
+ entry.tags.push("gravity_override");
2108
+ }
2109
+ }
2110
+ }
2070
2111
  /**
2071
2112
  * Update the background configuration at runtime.
2072
2113
  */
@@ -2124,7 +2165,10 @@ var OverlayScene = class {
2124
2165
  return result.id;
2125
2166
  }
2126
2167
  const id = crypto.randomUUID();
2127
- const tags = config.tags ?? [];
2168
+ const tags = [...config.tags ?? []];
2169
+ if (config.gravityOverride && !tags.includes("gravity_override")) {
2170
+ tags.push("gravity_override");
2171
+ }
2128
2172
  const isStatic = !tags.includes("falling");
2129
2173
  logger.debug("OverlayScene", `Spawning object`, {
2130
2174
  id,
@@ -2164,9 +2208,11 @@ var OverlayScene = class {
2164
2208
  pressureThreshold,
2165
2209
  shadow,
2166
2210
  originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
2167
- clicksRemaining
2211
+ clicksRemaining,
2212
+ gravityOverride: config.gravityOverride
2168
2213
  };
2169
2214
  this.objects.set(id, entry);
2215
+ if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
2170
2216
  import_matter_js5.default.Composite.add(this.engine.world, body);
2171
2217
  if (isStatic && pressureThreshold !== void 0) {
2172
2218
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
@@ -2180,7 +2226,10 @@ var OverlayScene = class {
2180
2226
  */
2181
2227
  async spawnObjectAsync(config) {
2182
2228
  const id = crypto.randomUUID();
2183
- const tags = config.tags ?? [];
2229
+ const tags = [...config.tags ?? []];
2230
+ if (config.gravityOverride && !tags.includes("gravity_override")) {
2231
+ tags.push("gravity_override");
2232
+ }
2184
2233
  const isStatic = !tags.includes("falling");
2185
2234
  logger.debug("OverlayScene", `Spawning object async`, {
2186
2235
  id,
@@ -2220,9 +2269,11 @@ var OverlayScene = class {
2220
2269
  pressureThreshold,
2221
2270
  shadow,
2222
2271
  originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
2223
- clicksRemaining
2272
+ clicksRemaining,
2273
+ gravityOverride: config.gravityOverride
2224
2274
  };
2225
2275
  this.objects.set(id, entry);
2276
+ if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
2226
2277
  import_matter_js5.default.Composite.add(this.engine.world, body);
2227
2278
  if (isStatic && pressureThreshold !== void 0) {
2228
2279
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
@@ -2311,6 +2362,7 @@ var OverlayScene = class {
2311
2362
  if (!entry) return;
2312
2363
  this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
2313
2364
  import_matter_js5.default.Composite.remove(this.engine.world, entry.body);
2365
+ this.gravityOverrideEntries.delete(entry);
2314
2366
  this.objects.delete(id);
2315
2367
  }
2316
2368
  removeObjects(ids) {
@@ -2323,6 +2375,7 @@ var OverlayScene = class {
2323
2375
  import_matter_js5.default.Composite.remove(this.engine.world, entry.body);
2324
2376
  }
2325
2377
  this.objects.clear();
2378
+ this.gravityOverrideEntries.clear();
2326
2379
  }
2327
2380
  removeObjectsByTag(tag) {
2328
2381
  const toRemove = [];
@@ -2392,8 +2445,7 @@ var OverlayScene = class {
2392
2445
  * @returns The ID of the grabbed object, or null if no grabable object at position
2393
2446
  */
2394
2447
  startGrab() {
2395
- const mouseTarget = this.followTargets.get("mouse");
2396
- const position = mouseTarget ?? (this.mouse ? { x: this.mouse.position.x, y: this.mouse.position.y } : null);
2448
+ const position = this.followTargets.get("mouse") ?? null;
2397
2449
  if (!position) return null;
2398
2450
  const bodies = import_matter_js5.default.Query.point(
2399
2451
  import_matter_js5.default.Composite.allBodies(this.engine.world),
@@ -3440,10 +3492,12 @@ var OverlayScene = class {
3440
3492
  var TAG_FALLING = "falling";
3441
3493
  var TAG_FOLLOW_WINDOW = "follow_window";
3442
3494
  var TAG_GRABABLE = "grabable";
3495
+ var TAG_GRAVITY_OVERRIDE = "gravity_override";
3443
3496
  var TAGS = {
3444
3497
  FALLING: TAG_FALLING,
3445
3498
  FOLLOW_WINDOW: TAG_FOLLOW_WINDOW,
3446
- GRABABLE: TAG_GRABABLE
3499
+ GRABABLE: TAG_GRABABLE,
3500
+ GRAVITY_OVERRIDE: TAG_GRAVITY_OVERRIDE
3447
3501
  };
3448
3502
  // Annotate the CommonJS export names for ESM import in node:
3449
3503
  0 && (module.exports = {
@@ -3453,6 +3507,7 @@ var TAGS = {
3453
3507
  TAG_FALLING,
3454
3508
  TAG_FOLLOW_WINDOW,
3455
3509
  TAG_GRABABLE,
3510
+ TAG_GRAVITY_OVERRIDE,
3456
3511
  clearFontCache,
3457
3512
  getGlyphData,
3458
3513
  getKerning,