@blorkfield/overlay-core 0.11.1 → 0.11.3

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
@@ -42,7 +42,7 @@ scene.spawnObject({ tags: [STATIC], ... });
42
42
  |----------|-------|----------|
43
43
  | `TAG_STATIC` / `TAGS.STATIC` | `'static'` | Object is a static obstacle, not affected by gravity. Without this tag, objects are dynamic by default. |
44
44
  | `TAG_FOLLOW_WINDOW` / `TAGS.FOLLOW_WINDOW` | `'follow_window'` | Object walks toward a target when grounded (default: mouse) |
45
- | `TAG_GRABABLE` / `TAGS.GRABABLE` | `'grabable'` | Object can be grabbed and moved with mouse |
45
+ | `TAG_GRABABLE` / `TAGS.GRABABLE` | `'grabable'` | Object can be grabbed and moved with mouse or touch |
46
46
  | `TAG_GRAVITY_OVERRIDE` / `TAGS.GRAVITY_OVERRIDE` | `'gravity_override'` | Object uses its own gravity vector instead of scene gravity |
47
47
  | `TAG_SPEED_OVERRIDE` / `TAGS.SPEED_OVERRIDE` | `'speed_override'` | Multiplies movement speed for `follow_window` and future movement behaviors. Negative = run away from target. |
48
48
  | `TAG_MASS_OVERRIDE` / `TAGS.MASS_OVERRIDE` | `'mass_override'` | Overrides the physics mass. Higher mass resists follow forces more; lower mass allows the follow force to overcome gravity. |
@@ -469,7 +469,9 @@ scene.setObjectScale(id, 2, 0.5); // stretch wide, squash tall
469
469
  scene.setObjectScale(id, 1, 1); // restore original size
470
470
  ```
471
471
 
472
- ## Mouse Position and Grab API
472
+ ## Mouse / Touch and Grab API
473
+
474
+ Mouse and touch input are handled automatically. On touch devices, a single finger grabs and drags `grabable` objects; a tap on a static object with `clicksToFall` set triggers the click-to-fall behavior. Two-finger gestures are ignored by the canvas so the browser can handle scroll and pinch-zoom normally. Touching over empty canvas (no `grabable` object) also passes through to the browser.
473
475
 
474
476
  For scenarios where mouse input comes from an external source (e.g., system-wide mouse capture via WebSocket), you can programmatically control mouse position and grab/release behavior. This is useful when the canvas is positioned with an offset from the screen origin.
475
477
 
@@ -802,7 +804,7 @@ Tags are the source of truth for all behavior. Boolean tags (presence = active,
802
804
  | Tag | Behavior |
803
805
  |-----|----------|
804
806
  | `static` | Static obstacle, not affected by gravity. Absent by default — objects are dynamic unless tagged static. |
805
- | `grabable` | Can be grabbed and dragged with the mouse |
807
+ | `grabable` | Can be grabbed and dragged with the mouse or touch |
806
808
  | `follow_window` | Always applies a directional force toward its target (in all axes). Gravity determines whether the entity can actually reach targets above/below it. |
807
809
  | `gravity_override` | Uses its own gravity vector instead of scene gravity (value set via `setObjectGravityOverride`) |
808
810
  | `speed_override` | Multiplies movement speed for `follow_window` and future movement behaviors (value set via `setObjectSpeedOverride`). Negative = runs away from target. Default multiplier: 1 |
package/dist/index.cjs CHANGED
@@ -513,6 +513,17 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
513
513
  render: renderOptions
514
514
  });
515
515
  import_matter_js2.default.Body.setPosition(body, { x, y });
516
+ if (!body.parts.every((part) => part.vertices && part.vertices.length >= 3)) {
517
+ logger.warn(LOG_PREFIX2, `fromVertices produced degenerate parts, falling back to circle`, { id });
518
+ return import_matter_js2.default.Bodies.circle(x, y, DEFAULT_RADIUS, {
519
+ restitution: 0.3,
520
+ friction: 0.1,
521
+ frictionAir: 0.01,
522
+ density: 5e-3,
523
+ label: `entity:${id}`,
524
+ render: renderOptions
525
+ });
526
+ }
516
527
  return body;
517
528
  }
518
529
  function createBoundariesWithFloorConfig(bounds, floorConfig) {
@@ -704,11 +715,26 @@ async function createBoxObstacleWithInfo(id, config, isStatic = true) {
704
715
  }
705
716
  }
706
717
  });
707
- const spriteOffsetX = (config.x - body.position.x) / scaledWidth;
708
- const spriteOffsetY = (config.y - body.position.y) / scaledHeight;
709
- if (body.render.sprite) {
710
- body.render.sprite.xOffset = 0.5 + spriteOffsetX;
711
- body.render.sprite.yOffset = 0.5 + spriteOffsetY;
718
+ if (!body.parts.every((part) => part.vertices && part.vertices.length >= 3)) {
719
+ logger.warn(LOG_PREFIX2, `fromVertices produced degenerate obstacle, falling back to rectangle`, { id });
720
+ body = import_matter_js2.default.Bodies.rectangle(config.x, config.y, scaledWidth, scaledHeight, {
721
+ isStatic,
722
+ label: `obstacle:${id}`,
723
+ render: {
724
+ sprite: {
725
+ texture: config.imageUrl,
726
+ xScale: spriteScale,
727
+ yScale: spriteScale
728
+ }
729
+ }
730
+ });
731
+ } else {
732
+ const spriteOffsetX = (config.x - body.position.x) / scaledWidth;
733
+ const spriteOffsetY = (config.y - body.position.y) / scaledHeight;
734
+ if (body.render.sprite) {
735
+ body.render.sprite.xOffset = 0.5 + spriteOffsetX;
736
+ body.render.sprite.yOffset = 0.5 + spriteOffsetY;
737
+ }
712
738
  }
713
739
  } else {
714
740
  body = import_matter_js2.default.Bodies.rectangle(config.x, config.y, scaledWidth, scaledHeight, {
@@ -757,6 +783,10 @@ async function createObstacleAsync(id, config, isStatic = true) {
757
783
  }
758
784
  });
759
785
  import_matter_js2.default.Body.setPosition(body, { x: config.x, y: config.y });
786
+ if (!body.parts.every((part) => part.vertices && part.vertices.length >= 3)) {
787
+ logger.warn(LOG_PREFIX2, `Image obstacle fromVertices degenerate, falling back to rectangle`, { id });
788
+ return createObstacle(id, config, isStatic);
789
+ }
760
790
  return body;
761
791
  }
762
792
  logger.warn(LOG_PREFIX2, `Image obstacle shape extraction failed, falling back to rectangle`, { id });
@@ -1492,6 +1522,10 @@ var OverlayScene = class {
1492
1522
  const rect = this.canvas.getBoundingClientRect();
1493
1523
  const x = event.clientX - rect.left;
1494
1524
  const y = event.clientY - rect.top;
1525
+ this.handleTap(x, y);
1526
+ };
1527
+ /** Shared tap logic for mouse click and touch tap */
1528
+ this.handleTap = (x, y) => {
1495
1529
  const bodies = import_matter_js5.default.Query.point(
1496
1530
  import_matter_js5.default.Composite.allBodies(this.engine.world),
1497
1531
  { x, y }
@@ -1509,6 +1543,42 @@ var OverlayScene = class {
1509
1543
  }
1510
1544
  }
1511
1545
  };
1546
+ /** Handle touch start — single finger grabs, multi-finger passes through for scroll/zoom */
1547
+ this.handleTouchStart = (event) => {
1548
+ if (event.touches.length >= 2) return;
1549
+ const touch = event.touches[0];
1550
+ const rect = this.canvas.getBoundingClientRect();
1551
+ const x = touch.clientX - rect.left;
1552
+ const y = touch.clientY - rect.top;
1553
+ this.followTargets.set("mouse", { x, y });
1554
+ const grabbed = this.startGrab();
1555
+ if (grabbed) event.preventDefault();
1556
+ };
1557
+ /** Handle touch move — only tracks if something is grabbed */
1558
+ this.handleTouchMove = (event) => {
1559
+ if (event.touches.length >= 2) return;
1560
+ if (!this.grabbedObjectId) return;
1561
+ event.preventDefault();
1562
+ const touch = event.touches[0];
1563
+ const rect = this.canvas.getBoundingClientRect();
1564
+ const x = touch.clientX - rect.left;
1565
+ const y = touch.clientY - rect.top;
1566
+ this.followTargets.set("mouse", { x, y });
1567
+ };
1568
+ /** Handle touch end/cancel — release grab; if it was a tap (no grab), trigger click-to-fall */
1569
+ this.handleTouchEnd = (event) => {
1570
+ const wasGrabbed = this.grabbedObjectId !== null;
1571
+ this.endGrab();
1572
+ if (wasGrabbed) {
1573
+ event.preventDefault();
1574
+ return;
1575
+ }
1576
+ if (event.changedTouches.length > 0) {
1577
+ const touch = event.changedTouches[0];
1578
+ const rect = this.canvas.getBoundingClientRect();
1579
+ this.handleTap(touch.clientX - rect.left, touch.clientY - rect.top);
1580
+ }
1581
+ };
1512
1582
  /**
1513
1583
  * Handler for Matter.js beforeRender event.
1514
1584
  * Draws base background layers (color + image) before physics objects.
@@ -1661,6 +1731,10 @@ var OverlayScene = class {
1661
1731
  canvas.addEventListener("mousemove", this.handleMouseMove);
1662
1732
  canvas.addEventListener("mouseup", this.handleMouseUp);
1663
1733
  canvas.addEventListener("click", this.handleCanvasClick);
1734
+ canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
1735
+ canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
1736
+ canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
1737
+ canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
1664
1738
  this.effectManager = new EffectManager(
1665
1739
  this.config.bounds,
1666
1740
  (cfg) => this.spawnObjectAsync(cfg),
@@ -2085,6 +2159,10 @@ var OverlayScene = class {
2085
2159
  this.canvas.removeEventListener("mousemove", this.handleMouseMove);
2086
2160
  this.canvas.removeEventListener("mouseup", this.handleMouseUp);
2087
2161
  this.canvas.removeEventListener("click", this.handleCanvasClick);
2162
+ this.canvas.removeEventListener("touchstart", this.handleTouchStart);
2163
+ this.canvas.removeEventListener("touchmove", this.handleTouchMove);
2164
+ this.canvas.removeEventListener("touchend", this.handleTouchEnd);
2165
+ this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
2088
2166
  import_matter_js5.default.Events.off(this.render, "beforeRender", this.handleBeforeRender);
2089
2167
  import_matter_js5.default.Events.off(this.render, "afterRender", this.handleAfterRender);
2090
2168
  import_matter_js5.default.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
@@ -3473,13 +3551,20 @@ var OverlayScene = class {
3473
3551
  x: currentX + v.x,
3474
3552
  y: currentY + v.y
3475
3553
  }));
3476
- const body = import_matter_js5.default.Bodies.fromVertices(glyphCenterX, glyphCenterY, [worldVertices], {
3554
+ let body = import_matter_js5.default.Bodies.fromVertices(glyphCenterX, glyphCenterY, [worldVertices], {
3477
3555
  isStatic,
3478
3556
  label: `obstacle:${id}`,
3479
3557
  render: {
3480
3558
  visible: false
3481
3559
  }
3482
3560
  });
3561
+ if (!body.parts.every((part) => part.vertices && part.vertices.length >= 3)) {
3562
+ body = import_matter_js5.default.Bodies.rectangle(glyphCenterX, glyphCenterY, Math.max(glyphWidth, 1), Math.max(Math.abs(glyphHeight), 1), {
3563
+ isStatic,
3564
+ label: `obstacle:${id}`,
3565
+ render: { visible: false }
3566
+ });
3567
+ }
3483
3568
  const offsetX = currentX - body.position.x;
3484
3569
  const offsetY = currentY - body.position.y;
3485
3570
  let pressureThreshold;