@peekling/runtime 0.1.0 → 0.1.2

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.
Files changed (52) hide show
  1. package/README.md +59 -8
  2. package/dist/canvas-renderer.d.ts +17 -0
  3. package/dist/canvas-renderer.d.ts.map +1 -0
  4. package/dist/canvas-renderer.js +192 -0
  5. package/dist/canvas.d.ts +5 -0
  6. package/dist/canvas.d.ts.map +1 -0
  7. package/dist/canvas.js +6 -0
  8. package/dist/configuration-validation.d.ts.map +1 -1
  9. package/dist/configuration-validation.js +176 -1
  10. package/dist/content.d.ts +3 -0
  11. package/dist/content.d.ts.map +1 -1
  12. package/dist/content.js +33 -7
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/input.d.ts.map +1 -1
  16. package/dist/input.js +4 -1
  17. package/dist/interaction.d.ts +37 -0
  18. package/dist/interaction.d.ts.map +1 -0
  19. package/dist/interaction.js +204 -0
  20. package/dist/overrides.d.ts +8 -0
  21. package/dist/overrides.d.ts.map +1 -1
  22. package/dist/overrides.js +18 -16
  23. package/dist/path-sampler.d.ts +9 -0
  24. package/dist/path-sampler.d.ts.map +1 -0
  25. package/dist/path-sampler.js +47 -0
  26. package/dist/peekling.css +1 -1
  27. package/dist/peekling.css.sri +1 -1
  28. package/dist/peekling.js +1365 -86
  29. package/dist/peekling.js.sri +1 -1
  30. package/dist/peekling.min.js +1 -1
  31. package/dist/peekling.min.js.sri +1 -1
  32. package/dist/plan-compiler.d.ts +2 -0
  33. package/dist/plan-compiler.d.ts.map +1 -1
  34. package/dist/plan-compiler.js +71 -14
  35. package/dist/plan.d.ts +27 -1
  36. package/dist/plan.d.ts.map +1 -1
  37. package/dist/plan.js +281 -22
  38. package/dist/registry.js +2 -2
  39. package/dist/renderer.d.ts +18 -1
  40. package/dist/renderer.d.ts.map +1 -1
  41. package/dist/renderer.js +1 -0
  42. package/dist/runtime-validation.d.ts.map +1 -1
  43. package/dist/runtime-validation.js +35 -0
  44. package/dist/runtime.d.ts +49 -3
  45. package/dist/runtime.d.ts.map +1 -1
  46. package/dist/runtime.js +365 -14
  47. package/dist/targets.d.ts +11 -0
  48. package/dist/targets.d.ts.map +1 -0
  49. package/dist/targets.js +106 -0
  50. package/dist/types.d.ts +61 -1
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +5 -1
package/dist/peekling.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @peekling/runtime 0.1.0 | Copyright 2026 Prajwal S. Venkateshmurthy | Apache-2.0 */
1
+ /*! @peekling/runtime 0.1.2 | Copyright 2026 Prajwal S. Venkateshmurthy | Apache-2.0 */
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/runtime/src/runtime-diagnostics.ts
@@ -748,6 +748,8 @@
748
748
  #name;
749
749
  #nameSelection;
750
750
  #nameItem;
751
+ #userHidden = false;
752
+ #hasVisibleSurface = false;
751
753
  #destroyed = false;
752
754
  constructor(document, options = {}) {
753
755
  this.#document = document;
@@ -819,6 +821,16 @@
819
821
  if (surface.seen !== generation) this.#removeSurface(surface);
820
822
  if (this.#destroyed) return;
821
823
  }
824
+ let visible = false;
825
+ for (const surface of this.#surfaces.values()) {
826
+ if (!surface.host.hidden) {
827
+ visible = true;
828
+ break;
829
+ }
830
+ }
831
+ this.#hasVisibleSurface = visible;
832
+ const hidden = this.#userHidden || !visible;
833
+ if (this.#host.hidden !== hidden) this.#host.hidden = hidden;
822
834
  const viewportChanged = viewport.width !== this.#viewportWidth || viewport.height !== this.#viewportHeight;
823
835
  const placementChanged = viewportChanged || position.x !== this.#positionX || position.y !== this.#positionY || character.width !== this.#characterWidth || character.height !== this.#characterHeight;
824
836
  this.#positionX = position.x;
@@ -900,14 +912,26 @@
900
912
  }
901
913
  this.#placementDirty = false;
902
914
  }
903
- let visible = false;
904
- for (const surface of this.#surfaces.values()) {
905
- if (!surface.host.hidden) {
906
- visible = true;
907
- break;
915
+ }
916
+ setUserHidden(hidden) {
917
+ if (this.#destroyed) return false;
918
+ const wasHidden = this.#userHidden;
919
+ this.#userHidden = hidden;
920
+ this.#host.hidden = hidden || !this.#hasVisibleSurface;
921
+ if (wasHidden && !hidden && !this.#host.hidden) {
922
+ for (const surface of this.#surfaces.values()) {
923
+ if (!surface.host.hidden) surface.measureDirty = true;
908
924
  }
925
+ this.#placementDirty = true;
926
+ this.#wake();
909
927
  }
910
- if (this.#host.hidden === visible) this.#host.hidden = !visible;
928
+ return !this.#userHidden;
929
+ }
930
+ toggleUserHidden() {
931
+ return this.setUserHidden(!this.#userHidden);
932
+ }
933
+ get userHidden() {
934
+ return this.#userHidden;
911
935
  }
912
936
  #select(selection, item, generation) {
913
937
  for (const region of CONTENT_REGIONS) {
@@ -1460,11 +1484,10 @@
1460
1484
  true
1461
1485
  );
1462
1486
  } else if (event === "window.scroll") {
1463
- listen(
1464
- this.#window,
1465
- "scroll",
1466
- (input) => this.#captureObserved("window.scroll", input)
1467
- );
1487
+ listen(this.#window, "scroll", (input) => {
1488
+ this.#clearPointer();
1489
+ this.#captureObserved("window.scroll", input);
1490
+ });
1468
1491
  } else if (event === "window.focus") {
1469
1492
  listen(
1470
1493
  this.#window,
@@ -1531,6 +1554,208 @@
1531
1554
  return incoming[policy.sessionField] === prior[policy.sessionField] && Number.isSafeInteger(incoming[policy.revisionField]) && Number.isSafeInteger(prior[policy.revisionField]) && incoming[policy.revisionField] > prior[policy.revisionField];
1532
1555
  }
1533
1556
 
1557
+ // packages/runtime/src/interaction.ts
1558
+ var CharacterInteractionController = class {
1559
+ ready;
1560
+ #document;
1561
+ #host;
1562
+ #root;
1563
+ #button;
1564
+ #badge;
1565
+ #styles;
1566
+ #options;
1567
+ #buttonRule;
1568
+ #badgeRule;
1569
+ #indicator;
1570
+ #label;
1571
+ #position = { x: 0, y: 0 };
1572
+ #pointerId;
1573
+ #startPointer = { x: 0, y: 0 };
1574
+ #lastPointer = { x: 0, y: 0 };
1575
+ #startPosition = { x: 0, y: 0 };
1576
+ #velocity = { x: 0, y: 0 };
1577
+ #lastTime = 0;
1578
+ #moved = false;
1579
+ #suppressClick = false;
1580
+ #destroyed = false;
1581
+ constructor(options) {
1582
+ this.#options = options;
1583
+ this.#document = options.document;
1584
+ this.#label = options.label;
1585
+ this.#host = options.document.createElement("div");
1586
+ this.#host.setAttribute("data-peekling-interaction", "");
1587
+ this.#root = this.#host.attachShadow({ mode: "closed" });
1588
+ this.#styles = new ShadowStyleSheet(
1589
+ options.document,
1590
+ this.#root,
1591
+ options.styles
1592
+ );
1593
+ this.#button = options.document.createElement("button");
1594
+ this.#button.type = "button";
1595
+ this.#button.className = "peekling-character-hit";
1596
+ this.#button.setAttribute("aria-label", options.label);
1597
+ if (options.expanded !== void 0) {
1598
+ this.#button.setAttribute("aria-expanded", String(options.expanded));
1599
+ }
1600
+ this.#button.toggleAttribute("data-peekling-draggable", options.draggable);
1601
+ this.#badge = options.document.createElement("span");
1602
+ this.#badge.className = "peekling-character-indicator";
1603
+ this.#badge.setAttribute("aria-hidden", "true");
1604
+ this.#button.append(this.#badge);
1605
+ this.#root.append(this.#button);
1606
+ this.ready = this.#styles.ready.then(() => {
1607
+ if (this.#destroyed) return;
1608
+ this.#buttonRule = this.#styles.addRule(".peekling-character-hit");
1609
+ this.#badgeRule = this.#styles.addRule(".peekling-character-indicator");
1610
+ this.#syncIndicatorColor();
1611
+ });
1612
+ void this.ready.catch(() => {
1613
+ });
1614
+ this.#button.addEventListener("click", this.#click);
1615
+ if (options.draggable) {
1616
+ this.#button.addEventListener("pointerdown", this.#pointerDown);
1617
+ this.#button.addEventListener("pointermove", this.#pointerMove);
1618
+ this.#button.addEventListener("pointerup", this.#pointerUp);
1619
+ this.#button.addEventListener("pointercancel", this.#pointerCancel);
1620
+ }
1621
+ this.updateIndicator(options.indicator);
1622
+ this.#repair();
1623
+ }
1624
+ render(position, character, lift = 0) {
1625
+ if (this.#destroyed) return;
1626
+ this.#repair();
1627
+ this.#position = { x: position.x, y: position.y - lift };
1628
+ const rule = this.#buttonRule;
1629
+ if (!rule) return;
1630
+ const left = position.x - character.width / 2;
1631
+ const top = position.y - character.height / 2 - lift;
1632
+ rule.width = `${character.width}px`;
1633
+ rule.height = `${character.height}px`;
1634
+ rule.transform = `translate3d(${left}px,${top}px,0)`;
1635
+ }
1636
+ setHidden(hidden) {
1637
+ if (!this.#destroyed) this.#host.toggleAttribute("hidden", hidden);
1638
+ }
1639
+ setExpanded(expanded) {
1640
+ if (!this.#destroyed && this.#button.hasAttribute("aria-expanded")) {
1641
+ this.#button.setAttribute("aria-expanded", String(expanded));
1642
+ }
1643
+ }
1644
+ updateIndicator(indicator) {
1645
+ if (this.#destroyed) return;
1646
+ this.#indicator = indicator ?? void 0;
1647
+ this.#syncIndicatorColor();
1648
+ const visible = Boolean(
1649
+ indicator && indicator.visible !== false && (indicator.kind !== "count" || (indicator.count ?? 0) > 0)
1650
+ );
1651
+ this.#badge.hidden = !visible;
1652
+ if (!visible || !indicator) {
1653
+ this.#badge.textContent = "";
1654
+ this.#button.setAttribute("aria-label", this.#label);
1655
+ return;
1656
+ }
1657
+ const kind = indicator.kind ?? "dot";
1658
+ this.#badge.dataset.kind = kind;
1659
+ this.#badge.textContent = kind === "count" ? String(Math.min(99, indicator.count ?? 0)) : "";
1660
+ this.#button.setAttribute(
1661
+ "aria-label",
1662
+ `${this.#label}. ${indicator.label}`
1663
+ );
1664
+ }
1665
+ #syncIndicatorColor() {
1666
+ if (this.#badgeRule) {
1667
+ this.#badgeRule.backgroundColor = this.#indicator?.color ?? "";
1668
+ }
1669
+ }
1670
+ destroy() {
1671
+ if (this.#destroyed) return;
1672
+ this.#destroyed = true;
1673
+ this.#styles.destroy();
1674
+ this.#host.remove();
1675
+ }
1676
+ #click = () => {
1677
+ if (this.#suppressClick) {
1678
+ this.#suppressClick = false;
1679
+ return;
1680
+ }
1681
+ this.#options.onPress();
1682
+ };
1683
+ #pointerDown = (event) => {
1684
+ if (this.#destroyed || this.#pointerId !== void 0 || !event.isPrimary && event.pointerType !== "mouse" || event.pointerType === "mouse" && event.button !== 0) {
1685
+ return;
1686
+ }
1687
+ this.#pointerId = event.pointerId;
1688
+ this.#startPointer = { x: event.clientX, y: event.clientY };
1689
+ this.#lastPointer = { ...this.#startPointer };
1690
+ this.#startPosition = { ...this.#position };
1691
+ this.#velocity = { x: 0, y: 0 };
1692
+ this.#lastTime = event.timeStamp;
1693
+ this.#moved = false;
1694
+ try {
1695
+ this.#button.setPointerCapture(event.pointerId);
1696
+ } catch {
1697
+ }
1698
+ this.#options.onDragStart();
1699
+ };
1700
+ #pointerMove = (event) => {
1701
+ if (event.pointerId !== this.#pointerId) return;
1702
+ const dx = event.clientX - this.#startPointer.x;
1703
+ const dy = event.clientY - this.#startPointer.y;
1704
+ if (!this.#moved && Math.hypot(dx, dy) >= 4) this.#moved = true;
1705
+ const elapsed = Math.max(
1706
+ 8,
1707
+ Math.min(100, event.timeStamp - this.#lastTime)
1708
+ );
1709
+ const sampleX = (event.clientX - this.#lastPointer.x) * 1e3 / elapsed;
1710
+ const sampleY = (event.clientY - this.#lastPointer.y) * 1e3 / elapsed;
1711
+ this.#velocity = {
1712
+ x: sampleX * 0.65 + this.#velocity.x * 0.35,
1713
+ y: sampleY * 0.65 + this.#velocity.y * 0.35
1714
+ };
1715
+ this.#lastPointer = { x: event.clientX, y: event.clientY };
1716
+ this.#lastTime = event.timeStamp;
1717
+ this.#options.onDragMove(
1718
+ {
1719
+ x: this.#startPosition.x + dx,
1720
+ y: this.#startPosition.y + dy
1721
+ },
1722
+ this.#velocity
1723
+ );
1724
+ };
1725
+ #pointerUp = (event) => {
1726
+ if (event.pointerId !== this.#pointerId) return;
1727
+ this.#finishPointer(event, this.#moved, this.#velocity);
1728
+ };
1729
+ #pointerCancel = (event) => {
1730
+ if (event.pointerId !== this.#pointerId) return;
1731
+ this.#finishPointer(event, this.#moved, { x: 0, y: 0 }, this.#lastPointer);
1732
+ };
1733
+ #finishPointer(event, moved, velocity, pointer = { x: event.clientX, y: event.clientY }) {
1734
+ const pointerId = this.#pointerId;
1735
+ this.#pointerId = void 0;
1736
+ if (pointerId !== void 0) {
1737
+ try {
1738
+ this.#button.releasePointerCapture(pointerId);
1739
+ } catch {
1740
+ }
1741
+ }
1742
+ this.#suppressClick = moved;
1743
+ this.#options.onDragEnd(velocity, moved, pointer);
1744
+ }
1745
+ #repair() {
1746
+ if (this.#destroyed) return;
1747
+ connectRuntimeHost(this.#host, this.#document, "data-peekling-interaction");
1748
+ if (this.#root.host !== this.#host || this.#button.getRootNode() !== this.#root) {
1749
+ throw new Error(
1750
+ runtimeMessage(
1751
+ "interaction.root",
1752
+ "Peekling interaction root structure was changed"
1753
+ )
1754
+ );
1755
+ }
1756
+ }
1757
+ };
1758
+
1534
1759
  // packages/runtime/src/direction.ts
1535
1760
  var OCTANTS = ["E", "SE", "S", "SW", "W", "NW", "N", "NE"];
1536
1761
  function directionTo(from, to) {
@@ -2949,8 +3174,8 @@
2949
3174
  }
2950
3175
 
2951
3176
  // packages/runtime/src/registry.ts
2952
- var PEEK_REFERENCE_URL = true ? "https://cdn.jsdelivr.net/npm/@peekling/pack-peek@0.1.0/character.json" : "https://cdn.jsdelivr.net/npm/@peekling/pack-peek@0.1.0/character.json";
2953
- var PEEK_REFERENCE_SHA256 = true ? "bc01840e84f72edca4b0421dca4d07623b38d971d851bfc647e1cada03e1cbcc" : "bc01840e84f72edca4b0421dca4d07623b38d971d851bfc647e1cada03e1cbcc";
3177
+ var PEEK_REFERENCE_URL = true ? "https://cdn.jsdelivr.net/npm/@peekling/pack-peek@0.1.1/character.json" : "https://cdn.jsdelivr.net/npm/@peekling/pack-peek@0.1.1/character.json";
3178
+ var PEEK_REFERENCE_SHA256 = true ? "9a2e2a41e85f7c4b8d3487a3655db40872675bd9c0780e139a946eabdf71d75f" : "9a2e2a41e85f7c4b8d3487a3655db40872675bd9c0780e139a946eabdf71d75f";
2954
3179
  var REGISTRY = {
2955
3180
  peek: Object.freeze({
2956
3181
  url: PEEK_REFERENCE_URL,
@@ -3840,7 +4065,7 @@
3840
4065
  );
3841
4066
  }
3842
4067
  const state = input.state === void 0 ? void 0 : compileState(input.state, `${path}.state`, context);
3843
- const motion = input.motion === void 0 ? void 0 : compileMotion(input.motion, `${path}.motion`);
4068
+ const motion = input.motion === void 0 ? void 0 : compileMotion(input.motion, `${path}.motion`, context);
3844
4069
  const surfaces = input.surfaces === void 0 ? [] : input.surfaces;
3845
4070
  if (!Array.isArray(surfaces) || surfaces.length > SURFACE_LIMIT) {
3846
4071
  fail(
@@ -4010,19 +4235,81 @@
4010
4235
  }
4011
4236
  return freeze({ kind: "capability", name: input.capability });
4012
4237
  }
4013
- function compileMotion(input, path) {
4238
+ function compileMotion(input, path, context) {
4014
4239
  object(input, path);
4015
- closed3(input, ["type", "speed", "arrivalRadius"], path);
4016
- if (input.type !== "follow-pointer") {
4017
- fail("invalid-motion", `${path}.type`, "must be follow-pointer");
4240
+ switch (input.type) {
4241
+ case "follow-pointer":
4242
+ closed3(input, ["type", "speed", "arrivalRadius"], path);
4243
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4244
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4245
+ break;
4246
+ case "horizontal-patrol":
4247
+ closed3(input, ["type", "speed", "edgeInset"], path);
4248
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4249
+ bounded(input.edgeInset, `${path}.edgeInset`, 0, 1e3);
4250
+ break;
4251
+ case "viewport-traverse":
4252
+ closed3(input, ["type", "speed", "edgeInset", "clockwise"], path);
4253
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4254
+ bounded(input.edgeInset, `${path}.edgeInset`, 0, 1e3);
4255
+ if (input.clockwise !== void 0 && typeof input.clockwise !== "boolean") {
4256
+ fail("invalid-motion", `${path}.clockwise`, "must be boolean");
4257
+ }
4258
+ break;
4259
+ case "move-to":
4260
+ closed3(input, ["type", "x", "y", "speed", "arrivalRadius"], path);
4261
+ bounded(input.x, `${path}.x`, -1e5, 1e5);
4262
+ bounded(input.y, `${path}.y`, -1e5, 1e5);
4263
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4264
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4265
+ break;
4266
+ case "move-to-target":
4267
+ closed3(
4268
+ input,
4269
+ ["type", "target", "anchor", "speed", "arrivalRadius"],
4270
+ path
4271
+ );
4272
+ id(input.target, `${path}.target`, "Target");
4273
+ if (context.targets && !context.targets.has(input.target)) {
4274
+ fail(
4275
+ "unknown-target",
4276
+ `${path}.target`,
4277
+ `references unknown target ${input.target}`
4278
+ );
4279
+ }
4280
+ if (input.anchor !== void 0 && !["center", "top", "right", "bottom", "left"].includes(input.anchor)) {
4281
+ fail("invalid-motion", `${path}.anchor`, "is not supported");
4282
+ }
4283
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4284
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4285
+ break;
4286
+ case "jump-to":
4287
+ closed3(input, ["type", "x", "y", "duration", "height"], path);
4288
+ bounded(input.x, `${path}.x`, -1e5, 1e5);
4289
+ bounded(input.y, `${path}.y`, -1e5, 1e5);
4290
+ bounded(input.duration, `${path}.duration`, 100, 1e4);
4291
+ bounded(input.height, `${path}.height`, 0, 2e3);
4292
+ break;
4293
+ case "svg-path": {
4294
+ closed3(input, ["type", "path", "duration", "loop", "relative"], path);
4295
+ if (typeof input.path !== "string" || input.path.length < 1 || input.path.length > 4096 || /[\0\r\n]/.test(input.path)) {
4296
+ fail("invalid-path", `${path}.path`, "must be bounded SVG path data");
4297
+ }
4298
+ if (context.validatePath && !context.validatePath(input.path)) {
4299
+ fail("invalid-path", `${path}.path`, "must be valid SVG path data");
4300
+ }
4301
+ bounded(input.duration, `${path}.duration`, 100, 6e4);
4302
+ for (const field of ["loop", "relative"]) {
4303
+ if (input[field] !== void 0 && typeof input[field] !== "boolean") {
4304
+ fail("invalid-motion", `${path}.${field}`, "must be boolean");
4305
+ }
4306
+ }
4307
+ break;
4308
+ }
4309
+ default:
4310
+ fail("invalid-motion", `${path}.type`, "is not a supported motion type");
4018
4311
  }
4019
- bounded(input.speed, `${path}.speed`, 1, 1e3);
4020
- bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4021
- return freeze({
4022
- type: "follow-pointer",
4023
- ...input.speed === void 0 ? {} : { speed: input.speed },
4024
- ...input.arrivalRadius === void 0 ? {} : { arrivalRadius: input.arrivalRadius }
4025
- });
4312
+ return freeze({ ...input });
4026
4313
  }
4027
4314
  function compileSurface(input, path, context, allowPayload) {
4028
4315
  object(input, path);
@@ -4363,7 +4650,25 @@
4363
4650
  thresholds: [...thresholds].sort((left, right) => left - right)
4364
4651
  };
4365
4652
  }
4366
- function createDefaultPlan(locomotion) {
4653
+ function createPresetPlan(preset, locomotion) {
4654
+ const state = locomotion ? { capability: "locomotion" } : { state: "idle" };
4655
+ if (preset === "still") {
4656
+ return {
4657
+ baseline: {
4658
+ channels: ["state"],
4659
+ state: { state: "idle" }
4660
+ }
4661
+ };
4662
+ }
4663
+ if (preset === "bottom-patrol" || preset === "viewport-roam") {
4664
+ return {
4665
+ baseline: {
4666
+ channels: ["motion", "state"],
4667
+ state,
4668
+ motion: preset === "bottom-patrol" ? { type: "horizontal-patrol", speed: DEFAULT_SPEED } : { type: "viewport-traverse", speed: DEFAULT_SPEED }
4669
+ }
4670
+ };
4671
+ }
4367
4672
  return {
4368
4673
  baseline: {
4369
4674
  channels: ["state"],
@@ -4389,6 +4694,12 @@
4389
4694
  #eventState = createPlanEventState();
4390
4695
  #activeChannels = /* @__PURE__ */ new Map();
4391
4696
  #activeSurfaces = /* @__PURE__ */ new Map();
4697
+ #patrolDirections = /* @__PURE__ */ new WeakMap();
4698
+ #traversePhases = /* @__PURE__ */ new WeakMap();
4699
+ #jumpOwner;
4700
+ #jumpState;
4701
+ #pathOwner;
4702
+ #pathState;
4392
4703
  #activeInterrupts = [];
4393
4704
  #surfaceVersion = 0;
4394
4705
  #lastEventStatus;
@@ -4476,24 +4787,81 @@
4476
4787
  if (state?.kind === "capability") {
4477
4788
  output.capability = state.name;
4478
4789
  }
4479
- const motion = channels.get("motion")?.motion;
4480
- if (motion && world.pointer) {
4481
- const dx = world.pointer.x - world.position.x;
4482
- const dy = world.pointer.y - world.position.y;
4483
- const distance = Math.hypot(dx, dy);
4484
- const arrivalRadius = motion.arrivalRadius ?? 0;
4485
- if (distance > arrivalRadius) {
4486
- output.motion = {
4487
- x: dx / distance,
4488
- y: dy / distance,
4489
- speed: motion.speed ?? DEFAULT_SPEED,
4490
- maxDistance: distance - arrivalRadius
4491
- };
4492
- } else if (channels.get("motion") === channels.get("state")) {
4493
- delete output.capability;
4494
- const baseline = this.#baselineState();
4495
- if (!output.state && baseline) output.state = baseline;
4496
- }
4790
+ const motionOwner = channels.get("motion");
4791
+ const motion = motionOwner?.motion;
4792
+ const settleCapability = () => {
4793
+ if (output.capability !== "locomotion") return;
4794
+ delete output.capability;
4795
+ const baseline = this.#baselineState();
4796
+ if (!output.state && baseline) output.state = baseline;
4797
+ };
4798
+ if (motion?.type !== "jump-to") {
4799
+ this.#jumpOwner = void 0;
4800
+ this.#jumpState = void 0;
4801
+ }
4802
+ if (motion?.type !== "svg-path") {
4803
+ this.#pathOwner = void 0;
4804
+ this.#pathState = void 0;
4805
+ }
4806
+ if (motion?.type === "horizontal-patrol" && world.characterSize) {
4807
+ const [direction, request] = evaluateHorizontalPatrol(
4808
+ motion,
4809
+ world,
4810
+ this.#patrolDirections.get(motionOwner) ?? true
4811
+ );
4812
+ this.#patrolDirections.set(motionOwner, direction);
4813
+ if (request) output.motion = request;
4814
+ } else if (motion?.type === "viewport-traverse" && world.characterSize) {
4815
+ const [phase, request] = evaluateViewportTraverse(
4816
+ motion,
4817
+ world,
4818
+ this.#traversePhases.get(motionOwner)
4819
+ );
4820
+ this.#traversePhases.set(motionOwner, phase);
4821
+ if (request) output.motion = request;
4822
+ } else if (motion?.type === "follow-pointer") {
4823
+ const request = world.pointer ? motionToward(
4824
+ motion,
4825
+ world.position,
4826
+ world.pointer.x,
4827
+ world.pointer.y,
4828
+ motion.arrivalRadius,
4829
+ world
4830
+ ) : void 0;
4831
+ if (request) output.motion = request;
4832
+ else settleCapability();
4833
+ } else if (motion?.type === "move-to") {
4834
+ const request = motionToward(
4835
+ motion,
4836
+ world.position,
4837
+ motion.x,
4838
+ motion.y,
4839
+ motion.arrivalRadius,
4840
+ world
4841
+ );
4842
+ if (request) output.motion = request;
4843
+ else settleCapability();
4844
+ } else if (motion?.type === "move-to-target") {
4845
+ const target = world.targets?.[motion.target];
4846
+ const point = target && world.characterSize ? targetPoint(target, motion.anchor, world.characterSize) : void 0;
4847
+ const request = point ? motionToward(
4848
+ motion,
4849
+ world.position,
4850
+ point.x,
4851
+ point.y,
4852
+ motion.arrivalRadius,
4853
+ world
4854
+ ) : void 0;
4855
+ if (request) output.motion = { ...request, trackTarget: true };
4856
+ else settleCapability();
4857
+ } else if (motion?.type === "jump-to") {
4858
+ const request = this.#evaluateJump(motionOwner, motion, world);
4859
+ if (request) output.motion = request;
4860
+ else settleCapability();
4861
+ } else if (motion?.type === "svg-path") {
4862
+ const request = this.#evaluatePath(motionOwner, motion, world);
4863
+ if (request) output.motion = request;
4864
+ else settleCapability();
4497
4865
  }
4498
4866
  const selected = /* @__PURE__ */ new Map();
4499
4867
  for (const surface of this.#plan.baseline.surfaces) {
@@ -4532,6 +4900,12 @@
4532
4900
  this.#activeChannels.clear();
4533
4901
  this.#activeSurfaces.clear();
4534
4902
  this.#activeInterrupts = [];
4903
+ this.#patrolDirections = /* @__PURE__ */ new WeakMap();
4904
+ this.#traversePhases = /* @__PURE__ */ new WeakMap();
4905
+ this.#jumpOwner = void 0;
4906
+ this.#jumpState = void 0;
4907
+ this.#pathOwner = void 0;
4908
+ this.#pathState = void 0;
4535
4909
  this.#eventState.ordering.clear();
4536
4910
  this.#eventState.sessions.clear();
4537
4911
  this.#lastEventStatus = void 0;
@@ -4557,17 +4931,75 @@
4557
4931
  const state = this.#plan.baseline.state;
4558
4932
  return state?.kind === "state" ? state.name : void 0;
4559
4933
  }
4934
+ #evaluateJump(owner, effect, world) {
4935
+ if (this.#jumpOwner !== owner || !this.#jumpState) {
4936
+ this.#jumpOwner = owner;
4937
+ this.#jumpState = {
4938
+ startedAt: world.now,
4939
+ from: { ...world.position }
4940
+ };
4941
+ }
4942
+ const duration = effect.duration ?? 800;
4943
+ const progress = Math.max(
4944
+ 0,
4945
+ Math.min(1, (world.now - this.#jumpState.startedAt) / duration)
4946
+ );
4947
+ const target = boundedPoint(effect, world);
4948
+ const desired = {
4949
+ x: this.#jumpState.from.x + (target.x - this.#jumpState.from.x) * progress,
4950
+ y: this.#jumpState.from.y + (target.y - this.#jumpState.from.y) * progress
4951
+ };
4952
+ const lift = progress === 0 || progress === 1 ? 0 : Math.sin(Math.PI * progress) * (effect.height ?? world.characterSize?.height ?? 32);
4953
+ const request = directMotion(world.position, desired, lift);
4954
+ if (progress < 1 || request.maxDistance) return request;
4955
+ return;
4956
+ }
4957
+ #evaluatePath(owner, effect, world) {
4958
+ const sample = world.samplePath;
4959
+ if (!sample) return;
4960
+ const first = sample(effect.path, 0);
4961
+ if (!first) return;
4962
+ if (this.#pathOwner !== owner || !this.#pathState) {
4963
+ this.#pathOwner = owner;
4964
+ this.#pathState = {
4965
+ startedAt: world.now,
4966
+ offset: effect.relative === false ? { x: 0, y: 0 } : {
4967
+ x: world.position.x - first.x,
4968
+ y: world.position.y - first.y
4969
+ }
4970
+ };
4971
+ }
4972
+ const duration = effect.duration ?? 4e3;
4973
+ const elapsed = Math.max(0, world.now - this.#pathState.startedAt);
4974
+ const complete = effect.loop !== true && elapsed >= duration;
4975
+ const progress = effect.loop ? elapsed % duration / duration : Math.min(1, elapsed / duration);
4976
+ const point = sample(effect.path, progress);
4977
+ if (!point) return;
4978
+ const desired = {
4979
+ x: point.x + this.#pathState.offset.x,
4980
+ y: point.y + this.#pathState.offset.y
4981
+ };
4982
+ const request = directMotion(world.position, boundedPoint(desired, world));
4983
+ if (!complete || request.maxDistance) return request;
4984
+ return;
4985
+ }
4560
4986
  #isContinuous(rule, world) {
4561
4987
  const condition = rule.when;
4562
4988
  if (condition.source !== "browser") return false;
4563
4989
  if (condition.event === "pointer.move") {
4564
4990
  if (!world.pointer) return false;
4565
4991
  const motion = rule.effect.motion;
4566
- if (!motion) return true;
4567
- return Math.hypot(
4568
- world.pointer.x - world.position.x,
4569
- world.pointer.y - world.position.y
4570
- ) > (motion.arrivalRadius ?? 0);
4992
+ if (!motion || motion.type !== "follow-pointer") return true;
4993
+ return Boolean(
4994
+ motionToward(
4995
+ motion,
4996
+ world.position,
4997
+ world.pointer.x,
4998
+ world.pointer.y,
4999
+ motion.arrivalRadius,
5000
+ world
5001
+ )
5002
+ );
4571
5003
  }
4572
5004
  if (condition.event !== "section.visibility") return false;
4573
5005
  const section = world.sections?.[condition.selector];
@@ -4610,6 +5042,128 @@
4610
5042
  function completionSource(name) {
4611
5043
  return browserCompletionEvent(name) ? "browser" : "application";
4612
5044
  }
5045
+ function evaluateHorizontalPatrol(effect, world, direction) {
5046
+ const halfWidth = world.characterSize.width / 2;
5047
+ const inset = effect.edgeInset ?? 0;
5048
+ const left = Math.min(world.viewport.width / 2, halfWidth + inset);
5049
+ const right = world.viewport.width - left;
5050
+ const halfHeight = world.characterSize.height / 2;
5051
+ const y = Math.max(halfHeight, world.viewport.height - halfHeight);
5052
+ let request = motionToward(
5053
+ effect,
5054
+ world.position,
5055
+ direction ? right : left,
5056
+ y
5057
+ );
5058
+ if (!request) {
5059
+ direction = !direction;
5060
+ request = motionToward(effect, world.position, direction ? right : left, y);
5061
+ }
5062
+ return [direction, request];
5063
+ }
5064
+ function evaluateViewportTraverse(effect, world, phase) {
5065
+ const inset = effect.edgeInset ?? 0;
5066
+ const halfWidth = world.characterSize.width / 2;
5067
+ const halfHeight = world.characterSize.height / 2;
5068
+ const left = Math.min(world.viewport.width / 2, halfWidth + inset);
5069
+ const right = Math.max(left, world.viewport.width - left);
5070
+ const top = Math.min(world.viewport.height / 2, halfHeight + inset);
5071
+ const bottom = Math.max(top, world.viewport.height - halfHeight - inset);
5072
+ const points = [
5073
+ { x: left, y: bottom },
5074
+ { x: right, y: bottom },
5075
+ { x: right, y: top },
5076
+ { x: left, y: top }
5077
+ ];
5078
+ if (phase === void 0) {
5079
+ phase = points.reduce(
5080
+ (nearest, point, index) => Math.hypot(point.x - world.position.x, point.y - world.position.y) < Math.hypot(
5081
+ points[nearest].x - world.position.x,
5082
+ points[nearest].y - world.position.y
5083
+ ) ? index : nearest,
5084
+ 0
5085
+ );
5086
+ }
5087
+ let target = points[phase];
5088
+ let request = motionToward(effect, world.position, target.x, target.y);
5089
+ if (!request) {
5090
+ const direction = effect.clockwise === false ? -1 : 1;
5091
+ phase = (phase + direction + points.length) % points.length;
5092
+ target = points[phase];
5093
+ request = motionToward(effect, world.position, target.x, target.y);
5094
+ }
5095
+ return [phase, request];
5096
+ }
5097
+ function targetPoint(target, anchor = "center", character) {
5098
+ const center = {
5099
+ x: target.left + target.width / 2,
5100
+ y: target.top + target.height / 2
5101
+ };
5102
+ switch (anchor) {
5103
+ case "top":
5104
+ return { x: center.x, y: target.top - character.height / 2 };
5105
+ case "right":
5106
+ return { x: target.right + character.width / 2, y: center.y };
5107
+ case "bottom":
5108
+ return { x: center.x, y: target.bottom + character.height / 2 };
5109
+ case "left":
5110
+ return { x: target.left - character.width / 2, y: center.y };
5111
+ default:
5112
+ return center;
5113
+ }
5114
+ }
5115
+ function boundedPoint(point, world) {
5116
+ const size = world.characterSize;
5117
+ if (!size) return { x: point.x, y: point.y };
5118
+ return {
5119
+ x: Math.max(
5120
+ size.width / 2,
5121
+ Math.min(world.viewport.width - size.width / 2, point.x)
5122
+ ),
5123
+ y: Math.max(
5124
+ size.height / 2,
5125
+ Math.min(world.viewport.height - size.height / 2, point.y)
5126
+ )
5127
+ };
5128
+ }
5129
+ function directMotion(position, target, lift = 0) {
5130
+ const dx = target.x - position.x;
5131
+ const dy = target.y - position.y;
5132
+ const distance = Math.hypot(dx, dy);
5133
+ return {
5134
+ x: distance ? dx / distance : 0,
5135
+ y: distance ? dy / distance : 0,
5136
+ speed: 0,
5137
+ maxDistance: distance,
5138
+ lift,
5139
+ direct: true
5140
+ };
5141
+ }
5142
+ function motionToward(effect, position, x, y, arrivalRadius = 0, bounds) {
5143
+ const size = bounds?.characterSize;
5144
+ const viewport = bounds?.viewport;
5145
+ if (size && viewport) {
5146
+ const pointerX = x;
5147
+ const pointerY = y;
5148
+ x = Math.max(size.width / 2, Math.min(viewport.width - size.width / 2, x));
5149
+ y = Math.max(
5150
+ size.height / 2,
5151
+ Math.min(viewport.height - size.height / 2, y)
5152
+ );
5153
+ if (x !== pointerX || y !== pointerY) arrivalRadius = 0;
5154
+ }
5155
+ const dx = x - position.x;
5156
+ const dy = y - position.y;
5157
+ const distance = Math.hypot(dx, dy);
5158
+ const maxDistance = distance - arrivalRadius;
5159
+ if (maxDistance <= 4) return;
5160
+ return {
5161
+ x: dx / distance,
5162
+ y: dy / distance,
5163
+ speed: effect.speed ?? DEFAULT_SPEED,
5164
+ maxDistance
5165
+ };
5166
+ }
4613
5167
 
4614
5168
  // packages/runtime/src/configuration-validation.ts
4615
5169
  var collectConfigurationFaults = !compactRuntimeDiagnostics;
@@ -4621,6 +5175,10 @@
4621
5175
  "atlasUrl",
4622
5176
  "styles",
4623
5177
  "plan",
5178
+ "preset",
5179
+ "targets",
5180
+ "interaction",
5181
+ "indicator",
4624
5182
  "content",
4625
5183
  "bindings",
4626
5184
  "theme",
@@ -4652,6 +5210,14 @@
4652
5210
  if (options.format !== void 0 && options.format !== 1) {
4653
5211
  reportConfigurationFault("unsupported-format", "$.format", fault);
4654
5212
  }
5213
+ if (options.plan !== void 0 && options.preset !== void 0) {
5214
+ reportConfigurationFault("incompatible-behavior", "$.preset", fault);
5215
+ }
5216
+ if (options.preset !== void 0 && !["companion", "still", "bottom-patrol", "viewport-roam"].includes(
5217
+ options.preset
5218
+ )) {
5219
+ reportConfigurationFault("invalid-preset", "$.preset", fault);
5220
+ }
4655
5221
  if (options.character !== void 0) {
4656
5222
  if (typeof options.character !== "string" || !NAME_PATTERN.test(options.character)) {
4657
5223
  reportConfigurationFault("invalid-character", "$.character", fault);
@@ -4737,6 +5303,12 @@
4737
5303
  if (collectConfigurationFaults) content = {};
4738
5304
  }
4739
5305
  validateBindings(options.bindings, content, fault);
5306
+ validateTargets(options.targets, fault);
5307
+ validateInteraction(options.interaction, options.targets, fault);
5308
+ validateIndicator(options.indicator, fault);
5309
+ if (options.interaction === false && options.indicator !== void 0) {
5310
+ reportConfigurationFault("incompatible-indicator", "$.indicator", fault);
5311
+ }
4740
5312
  validateTheme(options.theme, fault);
4741
5313
  validateDiagnostics(options.diagnostics, fault);
4742
5314
  validateAccessibility(options.accessibility, fault);
@@ -4747,6 +5319,201 @@
4747
5319
  }
4748
5320
  return content;
4749
5321
  }
5322
+ function validateInteraction(interaction, targets, fault) {
5323
+ if (interaction === void 0 || interaction === false) return;
5324
+ const checked = shape(
5325
+ interaction,
5326
+ "invalid-interaction",
5327
+ "$.interaction",
5328
+ [
5329
+ "press",
5330
+ "pressEvent",
5331
+ "drag",
5332
+ "throw",
5333
+ "dragState",
5334
+ "riseState",
5335
+ "fallState",
5336
+ "landState",
5337
+ "gravity",
5338
+ "maxThrowSpeed",
5339
+ "bounce",
5340
+ "floorInset",
5341
+ "label",
5342
+ "contentInitiallyHidden",
5343
+ "clearIndicatorOnPress",
5344
+ "catchTarget",
5345
+ "catchAnchor",
5346
+ "catchMargin",
5347
+ "catchEvent"
5348
+ ],
5349
+ fault
5350
+ );
5351
+ if (collectConfigurationFaults && !checked) return;
5352
+ const value = checked;
5353
+ if (value.press !== void 0 && ![
5354
+ "toggle-content",
5355
+ "show-content",
5356
+ "hide-content",
5357
+ "emit",
5358
+ "none"
5359
+ ].includes(value.press)) {
5360
+ reportConfigurationFault(
5361
+ "invalid-interaction",
5362
+ "$.interaction.press",
5363
+ fault
5364
+ );
5365
+ }
5366
+ if (value.pressEvent !== void 0 && (typeof value.pressEvent !== "string" || !EVENT_NAME_PATTERN.test(value.pressEvent))) {
5367
+ reportConfigurationFault(
5368
+ "invalid-interaction-event",
5369
+ "$.interaction.pressEvent",
5370
+ fault
5371
+ );
5372
+ }
5373
+ if (value.press === "emit" && value.pressEvent === void 0) {
5374
+ reportConfigurationFault(
5375
+ "missing-interaction-event",
5376
+ "$.interaction.pressEvent",
5377
+ fault
5378
+ );
5379
+ }
5380
+ if (value.pressEvent !== void 0 && value.press !== void 0 && value.press !== "emit") {
5381
+ reportConfigurationFault(
5382
+ "incompatible-interaction-event",
5383
+ "$.interaction.pressEvent",
5384
+ fault
5385
+ );
5386
+ }
5387
+ for (const field of [
5388
+ "drag",
5389
+ "throw",
5390
+ "contentInitiallyHidden",
5391
+ "clearIndicatorOnPress"
5392
+ ]) {
5393
+ if (value[field] !== void 0 && typeof value[field] !== "boolean") {
5394
+ reportConfigurationFault(
5395
+ "invalid-interaction",
5396
+ `$.interaction.${field}`,
5397
+ fault
5398
+ );
5399
+ }
5400
+ }
5401
+ for (const field of [
5402
+ "dragState",
5403
+ "riseState",
5404
+ "fallState",
5405
+ "landState"
5406
+ ]) {
5407
+ if (value[field] !== void 0 && (typeof value[field] !== "string" || !STATE_NAME_PATTERN.test(value[field]))) {
5408
+ reportConfigurationFault(
5409
+ "invalid-interaction-state",
5410
+ `$.interaction.${field}`,
5411
+ fault
5412
+ );
5413
+ }
5414
+ }
5415
+ for (const [field, minimum, maximum] of [
5416
+ ["gravity", 0, 1e4],
5417
+ ["maxThrowSpeed", 0, 1e4],
5418
+ ["bounce", 0, 1],
5419
+ ["floorInset", 0, 1e3],
5420
+ ["catchMargin", 0, 1e3]
5421
+ ]) {
5422
+ const item = value[field];
5423
+ if (item !== void 0 && (typeof item !== "number" || !Number.isFinite(item) || item < minimum || item > maximum)) {
5424
+ reportConfigurationFault(
5425
+ "invalid-interaction-range",
5426
+ `$.interaction.${field}`,
5427
+ fault
5428
+ );
5429
+ }
5430
+ }
5431
+ if (value.label !== void 0 && !text2(value.label, 120)) {
5432
+ reportConfigurationFault(
5433
+ "invalid-interaction-label",
5434
+ "$.interaction.label",
5435
+ fault
5436
+ );
5437
+ }
5438
+ if (value.catchTarget !== void 0 && (typeof value.catchTarget !== "string" || !NAME_PATTERN.test(value.catchTarget) || !Object.hasOwn(targets ?? {}, value.catchTarget))) {
5439
+ reportConfigurationFault(
5440
+ "invalid-catch-target",
5441
+ "$.interaction.catchTarget",
5442
+ fault
5443
+ );
5444
+ }
5445
+ if (value.catchAnchor !== void 0 && !["center", "top", "right", "bottom", "left"].includes(
5446
+ value.catchAnchor
5447
+ )) {
5448
+ reportConfigurationFault(
5449
+ "invalid-catch-anchor",
5450
+ "$.interaction.catchAnchor",
5451
+ fault
5452
+ );
5453
+ }
5454
+ if (value.catchEvent !== void 0 && (typeof value.catchEvent !== "string" || !EVENT_NAME_PATTERN.test(value.catchEvent))) {
5455
+ reportConfigurationFault(
5456
+ "invalid-interaction-event",
5457
+ "$.interaction.catchEvent",
5458
+ fault
5459
+ );
5460
+ }
5461
+ }
5462
+ function validateIndicator(indicator, fault) {
5463
+ if (indicator === void 0) return;
5464
+ const checked = shape(
5465
+ indicator,
5466
+ "invalid-indicator",
5467
+ "$.indicator",
5468
+ ["kind", "count", "label", "color", "visible"],
5469
+ fault
5470
+ );
5471
+ if (collectConfigurationFaults && !checked) return;
5472
+ const value = checked;
5473
+ if (value.kind !== void 0 && value.kind !== "dot" && value.kind !== "count") {
5474
+ reportConfigurationFault("invalid-indicator", "$.indicator.kind", fault);
5475
+ }
5476
+ if (!text2(value.label, 120)) {
5477
+ reportConfigurationFault("invalid-indicator", "$.indicator.label", fault);
5478
+ }
5479
+ if (value.count !== void 0 && (typeof value.count !== "number" || !Number.isInteger(value.count) || value.count < 0 || value.count > 999)) {
5480
+ reportConfigurationFault("invalid-indicator", "$.indicator.count", fault);
5481
+ }
5482
+ if (value.color !== void 0 && !isSurfaceColor(value.color)) {
5483
+ reportConfigurationFault("invalid-indicator", "$.indicator.color", fault);
5484
+ }
5485
+ if (value.visible !== void 0 && typeof value.visible !== "boolean") {
5486
+ reportConfigurationFault("invalid-indicator", "$.indicator.visible", fault);
5487
+ }
5488
+ }
5489
+ function validateTargets(targets, fault) {
5490
+ if (targets === void 0) return;
5491
+ const checked = shape(
5492
+ targets,
5493
+ "invalid-targets",
5494
+ "$.targets",
5495
+ void 0,
5496
+ fault
5497
+ );
5498
+ if (collectConfigurationFaults && !checked) return;
5499
+ const entries = Object.entries(checked);
5500
+ if (entries.length > 16) {
5501
+ reportConfigurationFault("target-limit", "$.targets", fault);
5502
+ }
5503
+ for (const [id2, selector2] of entries) {
5504
+ if (!NAME_PATTERN.test(id2)) {
5505
+ reportConfigurationFault("invalid-target", `$.targets.${id2}`, fault, id2);
5506
+ }
5507
+ if (typeof selector2 !== "string" || selector2.length < 1 || selector2.length > 256 || /[\0\r\n]/.test(selector2)) {
5508
+ reportConfigurationFault(
5509
+ "invalid-target-selector",
5510
+ `$.targets.${id2}`,
5511
+ fault,
5512
+ id2
5513
+ );
5514
+ }
5515
+ }
5516
+ }
4750
5517
  function validRuntimeName(value) {
4751
5518
  return value === void 0 || typeof value === "boolean" || text2(value, 80);
4752
5519
  }
@@ -4975,7 +5742,8 @@
4975
5742
  if (options.plan !== void 0) {
4976
5743
  try {
4977
5744
  compilePlan(options.plan, {
4978
- contentIds: new Set(Object.keys(content))
5745
+ contentIds: new Set(Object.keys(content)),
5746
+ targets: new Set(Object.keys(options.targets ?? {}))
4979
5747
  });
4980
5748
  } catch (cause) {
4981
5749
  fail2(
@@ -5056,6 +5824,40 @@
5056
5824
  return `${detail ?? field} must be a function`;
5057
5825
  case "invalid-plan":
5058
5826
  return detail ?? "Plan is invalid";
5827
+ case "incompatible-behavior":
5828
+ return "Choose preset or plan, not both";
5829
+ case "invalid-preset":
5830
+ return "preset must be companion, still, bottom-patrol, or viewport-roam";
5831
+ case "invalid-targets":
5832
+ return "targets must be an object of target IDs and CSS selectors";
5833
+ case "target-limit":
5834
+ return "targets supports at most 16 entries";
5835
+ case "invalid-target":
5836
+ return "target IDs must be bounded lowercase IDs";
5837
+ case "invalid-target-selector":
5838
+ return "target selectors must be bounded CSS selector text";
5839
+ case "invalid-interaction":
5840
+ return "interaction contains an unsupported value";
5841
+ case "invalid-interaction-event":
5842
+ return "interaction events must be bounded application Event names";
5843
+ case "missing-interaction-event":
5844
+ return "pressEvent is required when press is emit";
5845
+ case "incompatible-interaction-event":
5846
+ return "pressEvent can be used only with the emit press action";
5847
+ case "invalid-interaction-state":
5848
+ return "interaction states must be valid Pack State names";
5849
+ case "invalid-interaction-range":
5850
+ return "interaction physics value is outside its supported range";
5851
+ case "invalid-interaction-label":
5852
+ return "interaction label must be short plain text";
5853
+ case "invalid-catch-target":
5854
+ return "catchTarget must reference a configured target";
5855
+ case "invalid-catch-anchor":
5856
+ return "catchAnchor is not supported";
5857
+ case "invalid-indicator":
5858
+ return "indicator must include a label and valid dot or count settings";
5859
+ case "incompatible-indicator":
5860
+ return "indicator requires character interaction";
5059
5861
  default:
5060
5862
  return detail ?? code;
5061
5863
  }
@@ -5168,21 +5970,26 @@
5168
5970
  delete output.state;
5169
5971
  }
5170
5972
  const motion = item.effect.motion;
5171
- if (motion && world.pointer && world.position) {
5172
- const dx = world.pointer.x - world.position.x;
5173
- const dy = world.pointer.y - world.position.y;
5174
- const distance = Math.hypot(dx, dy);
5175
- const arrivalRadius = motion.arrivalRadius ?? 0;
5176
- if (distance > arrivalRadius) {
5177
- output.motion = {
5178
- x: dx / distance,
5179
- y: dy / distance,
5180
- speed: motion.speed ?? DEFAULT_SPEED,
5181
- maxDistance: distance - arrivalRadius
5182
- };
5183
- } else {
5184
- delete output.motion;
5185
- }
5973
+ if (motion?.type === "horizontal-patrol") {
5974
+ const [direction, request] = evaluateHorizontalPatrol(
5975
+ motion,
5976
+ world,
5977
+ item.patrolDirection
5978
+ );
5979
+ item.patrolDirection = direction;
5980
+ if (request) output.motion = request;
5981
+ else delete output.motion;
5982
+ } else if (motion?.type === "follow-pointer" && world.pointer && world.position) {
5983
+ const request = motionToward(
5984
+ motion,
5985
+ world.position,
5986
+ world.pointer.x,
5987
+ world.pointer.y,
5988
+ motion.arrivalRadius,
5989
+ world
5990
+ );
5991
+ if (request) output.motion = request;
5992
+ else delete output.motion;
5186
5993
  }
5187
5994
  for (const surface of item.effect.surfaces) {
5188
5995
  surfaces.set(
@@ -5249,6 +6056,7 @@
5249
6056
  startedAt: 0,
5250
6057
  deadline: 0,
5251
6058
  status: "rejected",
6059
+ patrolDirection: true,
5252
6060
  handle,
5253
6061
  finish
5254
6062
  });
@@ -5556,6 +6364,13 @@
5556
6364
  this.host.setAttribute("aria-hidden", "true");
5557
6365
  }
5558
6366
  };
6367
+ var createAtlasRenderer = (options) => new AtlasRenderer(
6368
+ options.document,
6369
+ options.pack,
6370
+ options.atlasUrl,
6371
+ options.scale,
6372
+ options.styles
6373
+ );
5559
6374
 
5560
6375
  // packages/runtime/src/resolver.ts
5561
6376
  var ALIASES = {
@@ -5799,6 +6614,142 @@
5799
6614
  }
5800
6615
  }
5801
6616
 
6617
+ // packages/runtime/src/path-sampler.ts
6618
+ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
6619
+ var SvgPathSampler = class {
6620
+ #document;
6621
+ #cache = /* @__PURE__ */ new Map();
6622
+ constructor(document) {
6623
+ this.#document = document;
6624
+ }
6625
+ validate(path) {
6626
+ return this.#geometry(path) !== void 0;
6627
+ }
6628
+ sample(path, progress) {
6629
+ const geometry = this.#geometry(path);
6630
+ if (!geometry) return;
6631
+ const bounded2 = Math.max(0, Math.min(1, progress));
6632
+ try {
6633
+ const point = geometry.element.getPointAtLength(
6634
+ geometry.length * bounded2
6635
+ );
6636
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
6637
+ return { x: point.x, y: point.y };
6638
+ } catch {
6639
+ return;
6640
+ }
6641
+ }
6642
+ #geometry(path) {
6643
+ const cached = this.#cache.get(path);
6644
+ if (cached) return cached;
6645
+ if (this.#cache.size >= 16) return;
6646
+ const element = this.#document.createElementNS(
6647
+ SVG_NAMESPACE,
6648
+ "path"
6649
+ );
6650
+ element.setAttribute("d", path);
6651
+ try {
6652
+ const length = element.getTotalLength();
6653
+ if (!Number.isFinite(length) || length <= 0) return;
6654
+ const geometry = Object.freeze({ element, length });
6655
+ this.#cache.set(path, geometry);
6656
+ return geometry;
6657
+ } catch {
6658
+ return;
6659
+ }
6660
+ }
6661
+ };
6662
+
6663
+ // packages/runtime/src/targets.ts
6664
+ var TargetTracker = class {
6665
+ #document;
6666
+ #window;
6667
+ #selectors;
6668
+ #elements = /* @__PURE__ */ new Map();
6669
+ #wake;
6670
+ #resizeObserver;
6671
+ #mutationObserver;
6672
+ #snapshot = Object.freeze({});
6673
+ #dirty = true;
6674
+ #destroyed = false;
6675
+ constructor(document, window, selectors, wake) {
6676
+ this.#document = document;
6677
+ this.#window = window;
6678
+ this.#selectors = selectors;
6679
+ this.#wake = wake;
6680
+ const browserWindow = window;
6681
+ this.#resizeObserver = browserWindow.ResizeObserver ? new browserWindow.ResizeObserver(() => this.refresh()) : void 0;
6682
+ this.#mutationObserver = browserWindow.MutationObserver ? new browserWindow.MutationObserver(() => this.refresh()) : void 0;
6683
+ this.#mutationObserver?.observe(document.documentElement, {
6684
+ attributes: true,
6685
+ attributeFilter: ["class", "id"],
6686
+ childList: true,
6687
+ subtree: true
6688
+ });
6689
+ window.addEventListener("resize", this.#markDirty, { passive: true });
6690
+ window.addEventListener("scroll", this.#markDirty, {
6691
+ capture: true,
6692
+ passive: true
6693
+ });
6694
+ }
6695
+ snapshot(force = false) {
6696
+ if (this.#destroyed) return Object.freeze({});
6697
+ if (force) this.#dirty = true;
6698
+ if (!this.#dirty) return this.#snapshot;
6699
+ const output = /* @__PURE__ */ Object.create(null);
6700
+ for (const [id2, selector2] of Object.entries(this.#selectors)) {
6701
+ let element = this.#elements.get(id2);
6702
+ if (!element?.isConnected) {
6703
+ if (element) this.#resizeObserver?.unobserve(element);
6704
+ element = this.#document.querySelector(selector2);
6705
+ if (element) {
6706
+ this.#elements.set(id2, element);
6707
+ this.#resizeObserver?.observe(element);
6708
+ } else {
6709
+ this.#elements.delete(id2);
6710
+ }
6711
+ }
6712
+ if (!element) continue;
6713
+ const rect = element.getBoundingClientRect();
6714
+ if (!Number.isFinite(rect.left) || !Number.isFinite(rect.top) || !Number.isFinite(rect.width) || !Number.isFinite(rect.height)) {
6715
+ continue;
6716
+ }
6717
+ output[id2] = Object.freeze({
6718
+ left: rect.left,
6719
+ top: rect.top,
6720
+ right: rect.right,
6721
+ bottom: rect.bottom,
6722
+ width: rect.width,
6723
+ height: rect.height
6724
+ });
6725
+ }
6726
+ this.#snapshot = Object.freeze(output);
6727
+ this.#dirty = false;
6728
+ return this.#snapshot;
6729
+ }
6730
+ contains(id2, point, margin = 0) {
6731
+ const target = this.snapshot(true)[id2];
6732
+ return Boolean(
6733
+ target && point.x >= target.left - margin && point.x <= target.right + margin && point.y >= target.top - margin && point.y <= target.bottom + margin
6734
+ );
6735
+ }
6736
+ refresh = () => {
6737
+ if (this.#destroyed) return;
6738
+ this.#dirty = true;
6739
+ this.#wake();
6740
+ };
6741
+ destroy() {
6742
+ if (this.#destroyed) return;
6743
+ this.#destroyed = true;
6744
+ this.#resizeObserver?.disconnect();
6745
+ this.#mutationObserver?.disconnect();
6746
+ this.#window.removeEventListener("resize", this.#markDirty);
6747
+ this.#window.removeEventListener("scroll", this.#markDirty, true);
6748
+ this.#elements.clear();
6749
+ }
6750
+ #markDirty = () => this.refresh();
6751
+ };
6752
+
5802
6753
  // packages/runtime/src/visibility.ts
5803
6754
  var PREFERENCE_KEY = "peekling:site-preference";
5804
6755
  var SESSION_KEY = "peekling:hidden-session";
@@ -6157,6 +7108,10 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6157
7108
  var SUSPEND_PAGE = 2;
6158
7109
  var SUSPEND_SITE = 4;
6159
7110
  var MAX_TIMER_DELAY = 2147483647;
7111
+ var DEFAULT_GRAVITY = 1800;
7112
+ var DEFAULT_THROW_SPEED = 1800;
7113
+ var DEFAULT_BOUNCE = 0.35;
7114
+ var DEFAULT_FLOOR_INSET = 8;
6160
7115
  var EMPTY_CONTENT_SELECTIONS = Object.freeze([]);
6161
7116
  function browserSelectorIsValid(document, selector2) {
6162
7117
  try {
@@ -6220,10 +7175,21 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6220
7175
  }
6221
7176
  return input;
6222
7177
  }
7178
+ function checkedIndicator(input) {
7179
+ if (input === null) return;
7180
+ const value = snapshotConfiguration(input);
7181
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.kind !== void 0 && value.kind !== "dot" && value.kind !== "count" || typeof value.label !== "string" || value.label.length < 1 || value.label.length > 120 || /[\u0000-\u001f\u007f]/.test(value.label) || value.count !== void 0 && (!Number.isInteger(value.count) || value.count < 0 || value.count > 999) || value.color !== void 0 && !isSurfaceColor(value.color) || value.visible !== void 0 && typeof value.visible !== "boolean") {
7182
+ throw new TypeError(
7183
+ runtimeMessage("indicator.invalid", "Indicator is invalid")
7184
+ );
7185
+ }
7186
+ return value;
7187
+ }
6223
7188
  var PeeklingRuntime = class _PeeklingRuntime {
6224
- static hatch(input) {
7189
+ static hatch(input, rendererFactory = createAtlasRenderer) {
6225
7190
  return new _PeeklingRuntime(
6226
- hatchOptions(input)
7191
+ hatchOptions(input),
7192
+ rendererFactory
6227
7193
  );
6228
7194
  }
6229
7195
  /** Package-internal presentation update used only by the Web Component. */
@@ -6259,7 +7225,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6259
7225
  #loaded;
6260
7226
  #renderer;
6261
7227
  #contentRenderer;
7228
+ #interactionController;
6262
7229
  #sections;
7230
+ #targets;
6263
7231
  #input;
6264
7232
  #raf = 0;
6265
7233
  #planTimer = 0;
@@ -6295,10 +7263,18 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6295
7263
  #instanceId;
6296
7264
  #diagnostics;
6297
7265
  #styles;
7266
+ #rendererFactory;
6298
7267
  #rejectedOverrideId = 0;
6299
7268
  #reportedNotReadyEvent = false;
6300
7269
  #reportedNotReadyOverride = false;
6301
- constructor(options = {}) {
7270
+ #trackTargets = false;
7271
+ #pathSampler;
7272
+ #indicator;
7273
+ #dragging = false;
7274
+ #throwing = false;
7275
+ #throwVelocity = { x: 0, y: 0 };
7276
+ #landUntil = 0;
7277
+ constructor(options = {}, rendererFactory = createAtlasRenderer) {
6302
7278
  options = snapshotConfiguration(options);
6303
7279
  const document = options.document ?? globalThis.document;
6304
7280
  const window = options.window ?? globalThis.window;
@@ -6308,7 +7284,10 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6308
7284
  );
6309
7285
  this.#document = document;
6310
7286
  this.#window = window;
7287
+ this.#pathSampler = new SvgPathSampler(document);
6311
7288
  this.#options = options;
7289
+ this.#indicator = options.indicator;
7290
+ this.#rendererFactory = rendererFactory;
6312
7291
  this.#now = window.performance?.now?.bind(window.performance) ?? Date.now;
6313
7292
  this.#instanceId = `instance:${Math.floor(this.#now())}:${Math.random().toString(36).slice(2, 8)}`;
6314
7293
  this.#diagnostics = new DiagnosticChannel({
@@ -6433,6 +7412,28 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6433
7412
  this.#setSuspended(SUSPEND_HOST, false);
6434
7413
  this.#notice("lifecycle.resumed", "info", "lifecycle");
6435
7414
  }
7415
+ refreshTargets() {
7416
+ this.#targets?.refresh();
7417
+ }
7418
+ setIndicator(indicator) {
7419
+ if (this.#destroyed) return;
7420
+ this.#indicator = checkedIndicator(indicator);
7421
+ this.#interactionController?.updateIndicator(this.#indicator);
7422
+ }
7423
+ setContentVisible(visible) {
7424
+ if (this.#destroyed || !this.#contentRenderer) return false;
7425
+ const expanded = this.#contentRenderer.setUserHidden(!visible);
7426
+ this.#interactionController?.setExpanded(expanded);
7427
+ this.#wake();
7428
+ return expanded;
7429
+ }
7430
+ toggleContent() {
7431
+ if (this.#destroyed || !this.#contentRenderer) return false;
7432
+ const expanded = this.#contentRenderer.toggleUserHidden();
7433
+ this.#interactionController?.setExpanded(expanded);
7434
+ this.#wake();
7435
+ return expanded;
7436
+ }
6436
7437
  destroy() {
6437
7438
  this.#destroy("destroyed");
6438
7439
  }
@@ -6446,7 +7447,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6446
7447
  this.#input?.destroy();
6447
7448
  this.#plan?.reset();
6448
7449
  this.#sections?.destroy();
7450
+ this.#targets?.destroy();
6449
7451
  this.#contentRenderer?.destroy();
7452
+ this.#interactionController?.destroy();
6450
7453
  this.#renderer?.destroy();
6451
7454
  this.#unregisterVisibility?.();
6452
7455
  this.#unregisterVisibility = void 0;
@@ -6476,10 +7479,20 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6476
7479
  async #initialize() {
6477
7480
  try {
6478
7481
  const validateSelector = (selector2) => browserSelectorIsValid(this.#document, selector2);
7482
+ const targetIds = new Set(Object.keys(this.#options.targets ?? {}));
7483
+ for (const selector2 of Object.values(this.#options.targets ?? {})) {
7484
+ if (!validateSelector(selector2)) {
7485
+ throw new TypeError(
7486
+ runtimeMessage("target.selector", "Target selector is invalid")
7487
+ );
7488
+ }
7489
+ }
6479
7490
  if (this.#options.plan !== void 0) {
6480
7491
  compilePlan(this.#options.plan, {
6481
7492
  contentIds: new Set(Object.keys(this.#content)),
6482
- validateSelector
7493
+ targets: targetIds,
7494
+ validateSelector,
7495
+ validatePath: (path) => this.#pathSampler.validate(path)
6483
7496
  });
6484
7497
  }
6485
7498
  const browserWindow = this.#window;
@@ -6547,12 +7560,14 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6547
7560
  locomotion ? ["locomotion"] : []
6548
7561
  );
6549
7562
  const compiled = compilePlan(
6550
- this.#options.plan ?? createDefaultPlan(locomotion),
7563
+ this.#options.plan ?? createPresetPlan(this.#options.preset ?? "companion", locomotion),
6551
7564
  {
6552
7565
  states: new Set(Object.keys(loaded.content.states)),
6553
7566
  capabilities,
6554
7567
  contentIds: new Set(Object.keys(this.#content)),
6555
- validateSelector
7568
+ targets: targetIds,
7569
+ validateSelector,
7570
+ validatePath: (path) => this.#pathSampler.validate(path)
6556
7571
  }
6557
7572
  );
6558
7573
  this.#plan = new PlanRuntime(compiled);
@@ -6560,7 +7575,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6560
7575
  {
6561
7576
  states: new Set(Object.keys(loaded.content.states)),
6562
7577
  capabilities,
6563
- contentIds: new Set(Object.keys(this.#content))
7578
+ contentIds: new Set(Object.keys(this.#content)),
7579
+ targets: targetIds,
7580
+ validatePath: (path) => this.#pathSampler.validate(path)
6564
7581
  },
6565
7582
  this.#now,
6566
7583
  (message) => this.#diagnose(message, "override.rejected", "warning", "override")
@@ -6588,21 +7605,45 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6588
7605
  (loaded.content.atlas.logicalWidth ?? loaded.content.atlas.cellWidth) * scale,
6589
7606
  (loaded.content.atlas.logicalHeight ?? loaded.content.atlas.cellHeight) * scale
6590
7607
  );
6591
- this.#renderer = new AtlasRenderer(
6592
- this.#document,
6593
- loaded.content,
6594
- loaded.atlasObjectUrl,
7608
+ this.#renderer = this.#rendererFactory({
7609
+ document: this.#document,
7610
+ pack: loaded.content,
7611
+ atlasUrl: loaded.atlasObjectUrl,
6595
7612
  scale,
6596
- this.#styles
6597
- );
7613
+ styles: this.#styles
7614
+ });
6598
7615
  if (Object.keys(this.#content).length || this.#displayName !== void 0) {
6599
7616
  this.#ensureContentRenderer();
6600
7617
  }
7618
+ if (this.#options.interaction !== false) {
7619
+ const interaction = this.#options.interaction ?? {};
7620
+ const pressAction = interaction.press ?? (interaction.pressEvent ? "emit" : "toggle-content");
7621
+ const controlsContent = Boolean(this.#contentRenderer) && (pressAction === "toggle-content" || pressAction === "show-content" || pressAction === "hide-content");
7622
+ if (interaction.contentInitiallyHidden) {
7623
+ this.#contentRenderer?.setUserHidden(true);
7624
+ }
7625
+ this.#interactionController = new CharacterInteractionController({
7626
+ document: this.#document,
7627
+ styles: this.#styles,
7628
+ label: interaction.label ?? `Interact with ${loaded.content.displayName || "Peekling"}`,
7629
+ draggable: interaction.drag !== false,
7630
+ ...controlsContent ? { expanded: !interaction.contentInitiallyHidden } : {},
7631
+ ...this.#indicator ? { indicator: this.#indicator } : {},
7632
+ onPress: () => this.#handleCharacterPress(),
7633
+ onDragStart: () => this.#startCharacterDrag(),
7634
+ onDragMove: (position, velocity) => this.#moveCharacterDrag(position, velocity),
7635
+ onDragEnd: (velocity, moved, pointer) => this.#endCharacterDrag(velocity, moved, pointer)
7636
+ });
7637
+ if (interaction.contentInitiallyHidden) {
7638
+ this.#interactionController.setExpanded(false);
7639
+ }
7640
+ }
6601
7641
  try {
6602
7642
  await Promise.all([
6603
7643
  this.#visibilityReady,
6604
7644
  this.#renderer.ready,
6605
- this.#contentRenderer?.ready
7645
+ this.#contentRenderer?.ready,
7646
+ this.#interactionController?.ready
6606
7647
  ]);
6607
7648
  } catch (cause) {
6608
7649
  this.#diagnoseStyleFailure(cause);
@@ -6610,6 +7651,14 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6610
7651
  }
6611
7652
  const requirements = compiledPlanSectionRequirements(compiled);
6612
7653
  this.#createInput();
7654
+ if (targetIds.size) {
7655
+ this.#targets = new TargetTracker(
7656
+ this.#document,
7657
+ this.#window,
7658
+ this.#options.targets,
7659
+ () => this.#wake()
7660
+ );
7661
+ }
6613
7662
  if (requirements.selectors.length) {
6614
7663
  this.#sections = new SectionTracker(
6615
7664
  this.#document,
@@ -6621,6 +7670,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6621
7670
  );
6622
7671
  }
6623
7672
  this.#renderer.setHidden(this.#siteHidden);
7673
+ this.#interactionController?.setHidden(this.#siteHidden);
6624
7674
  if (this.#suspensions) {
6625
7675
  this.#input?.setSuspended(true);
6626
7676
  this.#sections?.setSuspended(true);
@@ -6684,6 +7734,129 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6684
7734
  });
6685
7735
  return this.#contentRenderer;
6686
7736
  }
7737
+ #handleCharacterPress() {
7738
+ if (this.#destroyed) return;
7739
+ const interaction = this.#options.interaction || {};
7740
+ if (interaction.clearIndicatorOnPress === true && this.#indicator) {
7741
+ this.setIndicator(null);
7742
+ }
7743
+ const action = interaction.press ?? (interaction.pressEvent ? "emit" : "toggle-content");
7744
+ if (action === "emit") {
7745
+ if (interaction.pressEvent) this.emit(interaction.pressEvent);
7746
+ return;
7747
+ }
7748
+ if (action === "none") return;
7749
+ if (action === "show-content") this.setContentVisible(true);
7750
+ else if (action === "hide-content") this.setContentVisible(false);
7751
+ else this.toggleContent();
7752
+ }
7753
+ #startCharacterDrag() {
7754
+ if (this.#destroyed || this.#siteHidden) return;
7755
+ this.#dragging = true;
7756
+ this.#throwing = false;
7757
+ this.#landUntil = 0;
7758
+ this.#throwVelocity = { x: 0, y: 0 };
7759
+ this.#wake();
7760
+ }
7761
+ #moveCharacterDrag(position, velocity) {
7762
+ if (!this.#dragging || this.#destroyed) return;
7763
+ const size = this.#characterSize();
7764
+ this.#position = size ? clampPosition(
7765
+ position,
7766
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7767
+ size.width,
7768
+ size.height
7769
+ ) : { ...position };
7770
+ this.#throwVelocity = { ...velocity };
7771
+ this.#wake();
7772
+ }
7773
+ #endCharacterDrag(velocity, moved, pointer) {
7774
+ if (!this.#dragging || this.#destroyed) return;
7775
+ this.#dragging = false;
7776
+ if (!moved) {
7777
+ this.#throwVelocity = { x: 0, y: 0 };
7778
+ this.#wake();
7779
+ return;
7780
+ }
7781
+ const interaction = this.#options.interaction || {};
7782
+ if (interaction.catchTarget && this.#targets?.contains(
7783
+ interaction.catchTarget,
7784
+ pointer,
7785
+ interaction.catchMargin ?? 24
7786
+ ) && this.#catchCharacter(interaction.catchTarget)) {
7787
+ return;
7788
+ }
7789
+ if (this.#reducedMotion) {
7790
+ this.#landCharacter();
7791
+ return;
7792
+ }
7793
+ if (interaction.throw === false) {
7794
+ this.#throwVelocity = { x: 0, y: 0 };
7795
+ this.#wake();
7796
+ return;
7797
+ }
7798
+ const maximum = interaction.maxThrowSpeed ?? DEFAULT_THROW_SPEED;
7799
+ const magnitude = Math.hypot(velocity.x, velocity.y);
7800
+ const factor = magnitude > maximum && magnitude ? maximum / magnitude : 1;
7801
+ this.#throwVelocity = {
7802
+ x: velocity.x * factor,
7803
+ y: velocity.y * factor
7804
+ };
7805
+ this.#throwing = true;
7806
+ this.#wake();
7807
+ }
7808
+ #characterSize() {
7809
+ const loaded = this.#loaded?.content;
7810
+ if (!loaded) return;
7811
+ const scale = this.#options.scale ?? loaded.defaultScale;
7812
+ return {
7813
+ width: (loaded.atlas.logicalWidth ?? loaded.atlas.cellWidth) * scale,
7814
+ height: (loaded.atlas.logicalHeight ?? loaded.atlas.cellHeight) * scale
7815
+ };
7816
+ }
7817
+ #landCharacter() {
7818
+ const size = this.#characterSize();
7819
+ if (size) {
7820
+ const floorInset = (this.#options.interaction || {}).floorInset ?? DEFAULT_FLOOR_INSET;
7821
+ this.#position = clampPosition(
7822
+ {
7823
+ x: this.#position.x,
7824
+ y: this.#window.innerHeight - size.height / 2 - floorInset
7825
+ },
7826
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7827
+ size.width,
7828
+ size.height
7829
+ );
7830
+ }
7831
+ this.#dragging = false;
7832
+ this.#throwing = false;
7833
+ this.#throwVelocity = { x: 0, y: 0 };
7834
+ this.#landUntil = this.#now() + 360;
7835
+ this.#wake();
7836
+ }
7837
+ #catchCharacter(targetId) {
7838
+ const target = this.#targets?.snapshot(true)[targetId];
7839
+ const size = this.#characterSize();
7840
+ if (!target || !size) return false;
7841
+ const interaction = this.#options.interaction || {};
7842
+ const point = characterTargetPoint(
7843
+ target,
7844
+ interaction.catchAnchor ?? "center",
7845
+ size
7846
+ );
7847
+ this.#position = clampPosition(
7848
+ point,
7849
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7850
+ size.width,
7851
+ size.height
7852
+ );
7853
+ this.#throwing = false;
7854
+ this.#throwVelocity = { x: 0, y: 0 };
7855
+ this.#landUntil = this.#now() + 360;
7856
+ if (interaction.catchEvent) this.emit(interaction.catchEvent);
7857
+ this.#wake();
7858
+ return true;
7859
+ }
6687
7860
  #diagnose(message, code = "runtime.notice", severity = "warning", phase = "internal", eventId, metadata, cause) {
6688
7861
  this.#diagnostics.emit({
6689
7862
  code,
@@ -6770,6 +7943,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6770
7943
  if (hidden === this.#siteHidden) return;
6771
7944
  this.#siteHidden = hidden;
6772
7945
  this.#renderer?.setHidden(hidden);
7946
+ this.#interactionController?.setHidden(hidden);
6773
7947
  if (this.#destroyed) return;
6774
7948
  if (hidden && this.#contentRenderer) {
6775
7949
  this.#contentRenderer.renderSurfaces(
@@ -6785,6 +7959,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6785
7959
  if (this.#destroyed) return;
6786
7960
  }
6787
7961
  if (hidden) {
7962
+ this.#dragging = false;
7963
+ this.#throwing = false;
7964
+ this.#throwVelocity = { x: 0, y: 0 };
6788
7965
  this.#overrides?.clear("dismissed");
6789
7966
  this.#setSuspended(SUSPEND_SITE, true);
6790
7967
  this.#notice("lifecycle.dismissed", "info", "lifecycle");
@@ -6880,8 +8057,13 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6880
8057
  const loaded = this.#loaded;
6881
8058
  this.#densityUpgrade = loaded.upgrade(required).then((changed) => {
6882
8059
  if (changed && !this.#destroyed && this.#loaded === loaded) {
6883
- this.#renderer?.swapAtlas(loaded.content, loaded.atlasObjectUrl);
6884
- this.#wake();
8060
+ const renderer = this.#renderer;
8061
+ if (!renderer) return;
8062
+ return Promise.resolve(
8063
+ renderer.swapAtlas(loaded.content, loaded.atlasObjectUrl)
8064
+ ).then(() => {
8065
+ if (!this.#destroyed && this.#loaded === loaded) this.#wake();
8066
+ });
6885
8067
  }
6886
8068
  }).catch((error) => {
6887
8069
  if (!this.#destroyed)
@@ -6953,10 +8135,13 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6953
8135
  pointer: input.pointer,
6954
8136
  position: this.#position,
6955
8137
  viewport,
8138
+ characterSize,
6956
8139
  lastActivityAt: input.lastActivityAt,
6957
8140
  reducedMotion: this.#reducedMotion,
6958
8141
  pageVisible: !this.#document.hidden,
6959
8142
  sections,
8143
+ targets: this.#targets?.snapshot(this.#trackTargets),
8144
+ samplePath: (path, progress) => this.#pathSampler.sample(path, progress),
6960
8145
  reaction
6961
8146
  };
6962
8147
  return {
@@ -6977,11 +8162,26 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6977
8162
  overrides.observeEvent(reaction.name, reaction.source);
6978
8163
  }
6979
8164
  const request = overrides.compose(ordinary, world);
8165
+ this.#trackTargets = request.motion?.trackTarget === true;
6980
8166
  if (this.#reducedMotion) {
6981
8167
  delete request.motion;
6982
8168
  if (request.capability === "locomotion") delete request.capability;
6983
8169
  if (!request.state) request.state = "idle";
6984
8170
  }
8171
+ const interaction = this.#options.interaction || {};
8172
+ if (this.#dragging) {
8173
+ delete request.motion;
8174
+ delete request.capability;
8175
+ request.state = interaction.dragState ?? "scroll:fly";
8176
+ } else if (this.#throwing) {
8177
+ delete request.motion;
8178
+ delete request.capability;
8179
+ request.state = this.#throwVelocity.y < 0 ? interaction.riseState ?? "scroll:fly" : interaction.fallState ?? "scroll:fall";
8180
+ } else if (this.#landUntil > world.now) {
8181
+ delete request.motion;
8182
+ delete request.capability;
8183
+ request.state = interaction.landState ?? "success";
8184
+ }
6985
8185
  const diagnostics = [];
6986
8186
  if (reaction) {
6987
8187
  this.#seenReaction = reaction.id;
@@ -7040,19 +8240,74 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7040
8240
  }
7041
8241
  return this.#frameActive(frame);
7042
8242
  }
8243
+ #advanceThrow(snapshot) {
8244
+ if (!this.#throwing) return;
8245
+ const interaction = this.#options.interaction || {};
8246
+ const dt = snapshot.frame.stepMs / 1e3;
8247
+ const bounce = interaction.bounce ?? DEFAULT_BOUNCE;
8248
+ const halfWidth = snapshot.characterSize.width / 2;
8249
+ const halfHeight = snapshot.characterSize.height / 2;
8250
+ const floor = snapshot.world.viewport.height - halfHeight - (interaction.floorInset ?? DEFAULT_FLOOR_INSET);
8251
+ let velocityX = this.#throwVelocity.x;
8252
+ let velocityY = this.#throwVelocity.y + (interaction.gravity ?? DEFAULT_GRAVITY) * dt;
8253
+ let x = this.#position.x + velocityX * dt;
8254
+ let y = this.#position.y + velocityY * dt;
8255
+ const right = snapshot.world.viewport.width - halfWidth;
8256
+ if (x < halfWidth) {
8257
+ x = halfWidth;
8258
+ velocityX = Math.abs(velocityX) * bounce;
8259
+ } else if (x > right) {
8260
+ x = right;
8261
+ velocityX = -Math.abs(velocityX) * bounce;
8262
+ }
8263
+ if (y < halfHeight) {
8264
+ y = halfHeight;
8265
+ velocityY = Math.abs(velocityY) * bounce;
8266
+ }
8267
+ const next = { x, y };
8268
+ if (interaction.catchTarget && this.#targets?.contains(
8269
+ interaction.catchTarget,
8270
+ next,
8271
+ interaction.catchMargin ?? 24
8272
+ ) && this.#catchCharacter(interaction.catchTarget)) {
8273
+ return;
8274
+ }
8275
+ if (y >= floor) {
8276
+ this.#position = { x, y: floor };
8277
+ this.#landCharacter();
8278
+ return;
8279
+ }
8280
+ this.#position = next;
8281
+ this.#throwVelocity = { x: velocityX * 0.995, y: velocityY };
8282
+ }
7043
8283
  #calculatePresentation(snapshot, evaluation) {
7044
8284
  const { frame, characterSize, world } = snapshot;
7045
8285
  const { request } = evaluation;
7046
8286
  const loaded = frame.loaded.content;
7047
8287
  if (!this.#reducedMotion)
7048
8288
  this.#animationClock += Math.max(0, frame.elapsed);
8289
+ this.#advanceThrow(snapshot);
7049
8290
  const requested = request.capability === "locomotion" && request.motion ? loaded.directionalStates?.[directionTo(
7050
8291
  { x: 0, y: 0 },
7051
8292
  { x: request.motion.x, y: request.motion.y }
7052
8293
  )] : resolveCapabilityName(request.state, loaded);
7053
8294
  const resolved = resolveState(requested, loaded.states);
7054
8295
  let lift = 0;
7055
- if (request.motion) {
8296
+ if (request.motion?.direct) {
8297
+ const distance = request.motion.maxDistance ?? 0;
8298
+ this.#position = clampPosition(
8299
+ {
8300
+ x: this.#position.x + request.motion.x * distance,
8301
+ y: this.#position.y + request.motion.y * distance
8302
+ },
8303
+ world.viewport,
8304
+ characterSize.width,
8305
+ characterSize.height
8306
+ );
8307
+ lift = request.motion.lift ?? 0;
8308
+ this.#locomotionState = "";
8309
+ this.#locomotionElapsed = 0;
8310
+ } else if (request.motion) {
7056
8311
  if (this.#locomotionState !== resolved.name) {
7057
8312
  this.#locomotionState = resolved.name;
7058
8313
  this.#locomotionElapsed = 0;
@@ -7083,7 +8338,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7083
8338
  characterSize.width,
7084
8339
  characterSize.height
7085
8340
  );
7086
- lift = locomotionSample(loaded.locomotionMotion, nextCycles).lift * characterSize.height;
8341
+ lift = request.motion.lift ?? locomotionSample(loaded.locomotionMotion, nextCycles).lift * characterSize.height;
7087
8342
  } else {
7088
8343
  this.#locomotionState = "";
7089
8344
  this.#locomotionElapsed = 0;
@@ -7117,9 +8372,15 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7117
8372
  presentation.lift
7118
8373
  );
7119
8374
  if (!this.#frameActive(frame)) return;
8375
+ this.#interactionController?.render(
8376
+ this.#position,
8377
+ snapshot.characterSize,
8378
+ presentation.lift
8379
+ );
8380
+ if (!this.#frameActive(frame)) return;
7120
8381
  this.#lastTick = frame.now;
7121
8382
  this.#schedulePlan(frame);
7122
- if (this.#frameActive(frame) && (evaluation.request.motion || frame.input.reaction))
8383
+ if (this.#frameActive(frame) && (evaluation.request.motion || frame.input.reaction || this.#throwing || this.#dragging))
7123
8384
  this.#wake();
7124
8385
  }
7125
8386
  #endPage() {
@@ -7202,6 +8463,24 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7202
8463
  y: Math.max(height / 2, Math.min(viewport.height - height / 2, point.y))
7203
8464
  };
7204
8465
  }
8466
+ function characterTargetPoint(target, anchor, character) {
8467
+ const center = {
8468
+ x: target.left + target.width / 2,
8469
+ y: target.top + target.height / 2
8470
+ };
8471
+ switch (anchor) {
8472
+ case "top":
8473
+ return { x: center.x, y: target.top - character.height / 2 };
8474
+ case "right":
8475
+ return { x: target.right + character.width / 2, y: center.y };
8476
+ case "bottom":
8477
+ return { x: center.x, y: target.bottom + character.height / 2 };
8478
+ case "left":
8479
+ return { x: target.left - character.width / 2, y: center.y };
8480
+ default:
8481
+ return center;
8482
+ }
8483
+ }
7205
8484
 
7206
8485
  // packages/runtime/src/web-component.ts
7207
8486
  var PEEKLING_ELEMENT_TAG = "peekling-character";
@@ -7436,7 +8715,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7436
8715
  }
7437
8716
 
7438
8717
  // packages/runtime/src/browser.ts
7439
- var browserCssIntegrity = "sha256-w8BENjaJ/B1wtpk3iuh7jT++FlbT5+fx5BBlvePx1qo=" ? "sha256-w8BENjaJ/B1wtpk3iuh7jT++FlbT5+fx5BBlvePx1qo=" : void 0;
8718
+ var browserCssIntegrity = "sha256-r4PIKebZlQTb2JaHfQibQqQxHaKciwlXtTMDEvDTUPY=" ? "sha256-r4PIKebZlQTb2JaHfQibQqQxHaKciwlXtTMDEvDTUPY=" : void 0;
7440
8719
  var browserScript = globalThis.document?.currentScript;
7441
8720
  if (browserScript?.src) {
7442
8721
  setBrowserStyleAsset({