@blorkfield/overlay-core 0.4.3 → 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();
@@ -1498,6 +1501,15 @@ var OverlayScene = class {
1498
1501
  if (!entry.originalPosition) return;
1499
1502
  const opacity = entry.shadow?.opacity ?? 0.3;
1500
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
+ }
1501
1513
  if (entry.ttfGlyph) {
1502
1514
  const body = import_matter_js5.default.Bodies.circle(entry.originalPosition.x, entry.originalPosition.y, 1, {
1503
1515
  isStatic: true,
@@ -1971,6 +1983,84 @@ var OverlayScene = class {
1971
1983
  areFontsInitialized() {
1972
1984
  return this.fontsInitialized;
1973
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
+ }
1974
2064
  // ==================== TEXT OBSTACLE METHODS ====================
1975
2065
  /**
1976
2066
  * Create text obstacles from a string. Each character becomes an individual obstacle
@@ -2467,6 +2557,22 @@ var OverlayScene = class {
2467
2557
  ctx.restore();
2468
2558
  }
2469
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
+ }
2470
2576
  checkTTLExpiration() {
2471
2577
  const now = performance.now();
2472
2578
  const expiredObjects = [];