@blorkfield/overlay-core 0.8.11 → 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) {
@@ -1456,6 +1458,8 @@ var OverlayScene = class {
1456
1458
  this.grabHistoryRadius = 20;
1457
1459
  // Number of physics substeps per frame — more substeps = better collision at high speeds, more CPU
1458
1460
  this.substeps = 2;
1461
+ // Tracks only the bodies with a gravity override — engine gravity handles everyone else
1462
+ this.gravityOverrideEntries = /* @__PURE__ */ new Set();
1459
1463
  /** Handle mouse down - start grab via programmatic API */
1460
1464
  this.handleMouseDown = (event) => {
1461
1465
  const rect = this.canvas.getBoundingClientRect();
@@ -1533,7 +1537,16 @@ var OverlayScene = class {
1533
1537
  // ==================== PRIVATE ====================
1534
1538
  this.loop = () => {
1535
1539
  const substepDelta = 1e3 / 60 / this.substeps;
1540
+ const scale = this.engine.gravity.scale;
1536
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
+ }
1537
1550
  import_matter_js5.default.Engine.update(this.engine, substepDelta);
1538
1551
  }
1539
1552
  this.effectManager.update();
@@ -1596,7 +1609,7 @@ var OverlayScene = class {
1596
1609
  };
1597
1610
  this.canvas = canvas;
1598
1611
  this.config = {
1599
- gravity: 1,
1612
+ gravity: { x: 0, y: 1 },
1600
1613
  wrapHorizontal: true,
1601
1614
  debug: false,
1602
1615
  ...config
@@ -2062,6 +2075,39 @@ var OverlayScene = class {
2062
2075
  }
2063
2076
  }
2064
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
+ }
2065
2111
  /**
2066
2112
  * Update the background configuration at runtime.
2067
2113
  */
@@ -2119,7 +2165,10 @@ var OverlayScene = class {
2119
2165
  return result.id;
2120
2166
  }
2121
2167
  const id = crypto.randomUUID();
2122
- const tags = config.tags ?? [];
2168
+ const tags = [...config.tags ?? []];
2169
+ if (config.gravityOverride && !tags.includes("gravity_override")) {
2170
+ tags.push("gravity_override");
2171
+ }
2123
2172
  const isStatic = !tags.includes("falling");
2124
2173
  logger.debug("OverlayScene", `Spawning object`, {
2125
2174
  id,
@@ -2159,9 +2208,11 @@ var OverlayScene = class {
2159
2208
  pressureThreshold,
2160
2209
  shadow,
2161
2210
  originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
2162
- clicksRemaining
2211
+ clicksRemaining,
2212
+ gravityOverride: config.gravityOverride
2163
2213
  };
2164
2214
  this.objects.set(id, entry);
2215
+ if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
2165
2216
  import_matter_js5.default.Composite.add(this.engine.world, body);
2166
2217
  if (isStatic && pressureThreshold !== void 0) {
2167
2218
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
@@ -2175,7 +2226,10 @@ var OverlayScene = class {
2175
2226
  */
2176
2227
  async spawnObjectAsync(config) {
2177
2228
  const id = crypto.randomUUID();
2178
- const tags = config.tags ?? [];
2229
+ const tags = [...config.tags ?? []];
2230
+ if (config.gravityOverride && !tags.includes("gravity_override")) {
2231
+ tags.push("gravity_override");
2232
+ }
2179
2233
  const isStatic = !tags.includes("falling");
2180
2234
  logger.debug("OverlayScene", `Spawning object async`, {
2181
2235
  id,
@@ -2215,9 +2269,11 @@ var OverlayScene = class {
2215
2269
  pressureThreshold,
2216
2270
  shadow,
2217
2271
  originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
2218
- clicksRemaining
2272
+ clicksRemaining,
2273
+ gravityOverride: config.gravityOverride
2219
2274
  };
2220
2275
  this.objects.set(id, entry);
2276
+ if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
2221
2277
  import_matter_js5.default.Composite.add(this.engine.world, body);
2222
2278
  if (isStatic && pressureThreshold !== void 0) {
2223
2279
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
@@ -2306,6 +2362,7 @@ var OverlayScene = class {
2306
2362
  if (!entry) return;
2307
2363
  this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
2308
2364
  import_matter_js5.default.Composite.remove(this.engine.world, entry.body);
2365
+ this.gravityOverrideEntries.delete(entry);
2309
2366
  this.objects.delete(id);
2310
2367
  }
2311
2368
  removeObjects(ids) {
@@ -2318,6 +2375,7 @@ var OverlayScene = class {
2318
2375
  import_matter_js5.default.Composite.remove(this.engine.world, entry.body);
2319
2376
  }
2320
2377
  this.objects.clear();
2378
+ this.gravityOverrideEntries.clear();
2321
2379
  }
2322
2380
  removeObjectsByTag(tag) {
2323
2381
  const toRemove = [];
@@ -3434,10 +3492,12 @@ var OverlayScene = class {
3434
3492
  var TAG_FALLING = "falling";
3435
3493
  var TAG_FOLLOW_WINDOW = "follow_window";
3436
3494
  var TAG_GRABABLE = "grabable";
3495
+ var TAG_GRAVITY_OVERRIDE = "gravity_override";
3437
3496
  var TAGS = {
3438
3497
  FALLING: TAG_FALLING,
3439
3498
  FOLLOW_WINDOW: TAG_FOLLOW_WINDOW,
3440
- GRABABLE: TAG_GRABABLE
3499
+ GRABABLE: TAG_GRABABLE,
3500
+ GRAVITY_OVERRIDE: TAG_GRAVITY_OVERRIDE
3441
3501
  };
3442
3502
  // Annotate the CommonJS export names for ESM import in node:
3443
3503
  0 && (module.exports = {
@@ -3447,6 +3507,7 @@ var TAGS = {
3447
3507
  TAG_FALLING,
3448
3508
  TAG_FOLLOW_WINDOW,
3449
3509
  TAG_GRABABLE,
3510
+ TAG_GRAVITY_OVERRIDE,
3450
3511
  clearFontCache,
3451
3512
  getGlyphData,
3452
3513
  getKerning,