@blorkfield/overlay-core 0.4.2 → 0.5.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/dist/index.cjs CHANGED
@@ -1204,6 +1204,9 @@ var OverlayScene = class {
1204
1204
  if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
1205
1205
  wrapHorizontal(entry.body, this.config.bounds);
1206
1206
  }
1207
+ if (entry.domElement && entry.tags.includes("falling")) {
1208
+ this.updateDOMElementTransform(entry);
1209
+ }
1207
1210
  }
1208
1211
  if (!this.config.debug) {
1209
1212
  this.drawTTFGlyphs();
@@ -1238,6 +1241,13 @@ var OverlayScene = class {
1238
1241
  }
1239
1242
  });
1240
1243
  import_matter_js5.default.Composite.add(this.engine.world, this.mouseConstraint);
1244
+ const wheelHandler = this.mouse.mousewheel;
1245
+ if (wheelHandler) {
1246
+ canvas.removeEventListener("mousewheel", wheelHandler);
1247
+ canvas.removeEventListener("DOMMouseScroll", wheelHandler);
1248
+ canvas.removeEventListener("wheel", wheelHandler);
1249
+ }
1250
+ canvas.style.touchAction = "pan-x pan-y";
1241
1251
  import_matter_js5.default.Events.on(this.mouseConstraint, "startdrag", this.handleStartDrag);
1242
1252
  canvas.addEventListener("click", this.handleCanvasClick);
1243
1253
  this.render.mouse = this.mouse;
@@ -1491,6 +1501,15 @@ var OverlayScene = class {
1491
1501
  if (!entry.originalPosition) return;
1492
1502
  const opacity = entry.shadow?.opacity ?? 0.3;
1493
1503
  const shadowId = `shadow-${entry.id}`;
1504
+ if (entry.domElement) {
1505
+ const shadowElement = entry.domElement.cloneNode(true);
1506
+ shadowElement.style.opacity = String(opacity);
1507
+ shadowElement.style.pointerEvents = "none";
1508
+ shadowElement.style.transform = entry.domOriginalTransform || "";
1509
+ entry.domElement.parentNode?.insertBefore(shadowElement, entry.domElement);
1510
+ entry.domShadowElement = shadowElement;
1511
+ return;
1512
+ }
1494
1513
  if (entry.ttfGlyph) {
1495
1514
  const body = import_matter_js5.default.Bodies.circle(entry.originalPosition.x, entry.originalPosition.y, 1, {
1496
1515
  isStatic: true,
@@ -1964,6 +1983,84 @@ var OverlayScene = class {
1964
1983
  areFontsInitialized() {
1965
1984
  return this.fontsInitialized;
1966
1985
  }
1986
+ // ==================== DOM OBSTACLE METHODS ====================
1987
+ /**
1988
+ * Attach a DOM element to physics. The element will follow the physics body
1989
+ * and can have pressure threshold, shadow, and click-to-fall behavior.
1990
+ *
1991
+ * When the element collapses (becomes dynamic), its CSS transform will be
1992
+ * updated each frame to match the physics body position and rotation.
1993
+ *
1994
+ * Shadow creates a cloned DOM element that stays at the original position.
1995
+ */
1996
+ addDOMObstacle(config) {
1997
+ const { element, x, y } = config;
1998
+ const width = config.width ?? element.offsetWidth;
1999
+ const height = config.height ?? element.offsetHeight;
2000
+ const tags = config.tags ?? [];
2001
+ const isStatic = !tags.includes("falling");
2002
+ const body = import_matter_js5.default.Bodies.rectangle(x, y, width, height, {
2003
+ isStatic,
2004
+ label: `dom-${crypto.randomUUID().slice(0, 8)}`,
2005
+ render: { visible: false }
2006
+ // Don't render the body, DOM element is the visual
2007
+ });
2008
+ const id = body.label;
2009
+ let pressureThreshold;
2010
+ if (config.pressureThreshold) {
2011
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
2012
+ }
2013
+ const shadow = config.shadow ? { opacity: config.shadow.opacity ?? 0.3 } : void 0;
2014
+ const clicksRemaining = config.clickToFall?.clicks;
2015
+ const originalTransform = element.style.transform || "";
2016
+ element.style.position = "absolute";
2017
+ element.style.transformOrigin = "center center";
2018
+ const entry = {
2019
+ id,
2020
+ body,
2021
+ tags,
2022
+ spawnTime: performance.now(),
2023
+ pressureThreshold,
2024
+ weight: config.weight ?? 1,
2025
+ shadow,
2026
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x, y } : void 0,
2027
+ clicksRemaining,
2028
+ domElement: element,
2029
+ domOriginalTransform: originalTransform
2030
+ };
2031
+ this.objects.set(id, entry);
2032
+ import_matter_js5.default.Composite.add(this.engine.world, body);
2033
+ if (isStatic && pressureThreshold !== void 0) {
2034
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2035
+ }
2036
+ if (clicksRemaining !== void 0) {
2037
+ const clickHandler = () => {
2038
+ const currentEntry = this.objects.get(id);
2039
+ if (!currentEntry) return;
2040
+ if (currentEntry.tags.includes("falling")) return;
2041
+ if (currentEntry.clicksRemaining === void 0) return;
2042
+ currentEntry.clicksRemaining--;
2043
+ logger.debug("OverlayScene", `Click on DOM element: ${currentEntry.clicksRemaining} clicks remaining`);
2044
+ if (currentEntry.clicksRemaining <= 0) {
2045
+ this.collapseObstacle(currentEntry);
2046
+ element.removeEventListener("click", clickHandler);
2047
+ }
2048
+ };
2049
+ element.addEventListener("click", clickHandler);
2050
+ }
2051
+ return {
2052
+ id,
2053
+ shadowElement: null
2054
+ // Will be populated on collapse
2055
+ };
2056
+ }
2057
+ /**
2058
+ * Get the shadow element for a DOM obstacle (available after collapse).
2059
+ */
2060
+ getDOMObstacleShadow(id) {
2061
+ const entry = this.objects.get(id);
2062
+ return entry?.domShadowElement ?? null;
2063
+ }
1967
2064
  // ==================== TEXT OBSTACLE METHODS ====================
1968
2065
  /**
1969
2066
  * Create text obstacles from a string. Each character becomes an individual obstacle
@@ -2460,6 +2557,22 @@ var OverlayScene = class {
2460
2557
  ctx.restore();
2461
2558
  }
2462
2559
  }
2560
+ /**
2561
+ * Update a DOM element's CSS transform to match its physics body position and rotation.
2562
+ */
2563
+ updateDOMElementTransform(entry) {
2564
+ if (!entry.domElement) return;
2565
+ const body = entry.body;
2566
+ const x = body.position.x;
2567
+ const y = body.position.y;
2568
+ const angle = body.angle;
2569
+ const angleDeg = angle * (180 / Math.PI);
2570
+ const width = entry.domElement.offsetWidth;
2571
+ const height = entry.domElement.offsetHeight;
2572
+ entry.domElement.style.left = `${x - width / 2}px`;
2573
+ entry.domElement.style.top = `${y - height / 2}px`;
2574
+ entry.domElement.style.transform = `rotate(${angleDeg}deg)`;
2575
+ }
2463
2576
  checkTTLExpiration() {
2464
2577
  const now = performance.now();
2465
2578
  const expiredObjects = [];