@peekling/runtime 0.1.0 → 0.1.1

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 +207 -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 +1368 -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.1 | 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,211 @@
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, false, { x: 0, y: 0 });
1732
+ };
1733
+ #finishPointer(event, moved, velocity) {
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, {
1744
+ x: event.clientX,
1745
+ y: event.clientY
1746
+ });
1747
+ }
1748
+ #repair() {
1749
+ if (this.#destroyed) return;
1750
+ connectRuntimeHost(this.#host, this.#document, "data-peekling-interaction");
1751
+ if (this.#root.host !== this.#host || this.#button.getRootNode() !== this.#root) {
1752
+ throw new Error(
1753
+ runtimeMessage(
1754
+ "interaction.root",
1755
+ "Peekling interaction root structure was changed"
1756
+ )
1757
+ );
1758
+ }
1759
+ }
1760
+ };
1761
+
1534
1762
  // packages/runtime/src/direction.ts
1535
1763
  var OCTANTS = ["E", "SE", "S", "SW", "W", "NW", "N", "NE"];
1536
1764
  function directionTo(from, to) {
@@ -2949,8 +3177,8 @@
2949
3177
  }
2950
3178
 
2951
3179
  // 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";
3180
+ 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";
3181
+ var PEEK_REFERENCE_SHA256 = true ? "9a2e2a41e85f7c4b8d3487a3655db40872675bd9c0780e139a946eabdf71d75f" : "9a2e2a41e85f7c4b8d3487a3655db40872675bd9c0780e139a946eabdf71d75f";
2954
3182
  var REGISTRY = {
2955
3183
  peek: Object.freeze({
2956
3184
  url: PEEK_REFERENCE_URL,
@@ -3840,7 +4068,7 @@
3840
4068
  );
3841
4069
  }
3842
4070
  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`);
4071
+ const motion = input.motion === void 0 ? void 0 : compileMotion(input.motion, `${path}.motion`, context);
3844
4072
  const surfaces = input.surfaces === void 0 ? [] : input.surfaces;
3845
4073
  if (!Array.isArray(surfaces) || surfaces.length > SURFACE_LIMIT) {
3846
4074
  fail(
@@ -4010,19 +4238,81 @@
4010
4238
  }
4011
4239
  return freeze({ kind: "capability", name: input.capability });
4012
4240
  }
4013
- function compileMotion(input, path) {
4241
+ function compileMotion(input, path, context) {
4014
4242
  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");
4243
+ switch (input.type) {
4244
+ case "follow-pointer":
4245
+ closed3(input, ["type", "speed", "arrivalRadius"], path);
4246
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4247
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4248
+ break;
4249
+ case "horizontal-patrol":
4250
+ closed3(input, ["type", "speed", "edgeInset"], path);
4251
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4252
+ bounded(input.edgeInset, `${path}.edgeInset`, 0, 1e3);
4253
+ break;
4254
+ case "viewport-traverse":
4255
+ closed3(input, ["type", "speed", "edgeInset", "clockwise"], path);
4256
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4257
+ bounded(input.edgeInset, `${path}.edgeInset`, 0, 1e3);
4258
+ if (input.clockwise !== void 0 && typeof input.clockwise !== "boolean") {
4259
+ fail("invalid-motion", `${path}.clockwise`, "must be boolean");
4260
+ }
4261
+ break;
4262
+ case "move-to":
4263
+ closed3(input, ["type", "x", "y", "speed", "arrivalRadius"], path);
4264
+ bounded(input.x, `${path}.x`, -1e5, 1e5);
4265
+ bounded(input.y, `${path}.y`, -1e5, 1e5);
4266
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4267
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4268
+ break;
4269
+ case "move-to-target":
4270
+ closed3(
4271
+ input,
4272
+ ["type", "target", "anchor", "speed", "arrivalRadius"],
4273
+ path
4274
+ );
4275
+ id(input.target, `${path}.target`, "Target");
4276
+ if (context.targets && !context.targets.has(input.target)) {
4277
+ fail(
4278
+ "unknown-target",
4279
+ `${path}.target`,
4280
+ `references unknown target ${input.target}`
4281
+ );
4282
+ }
4283
+ if (input.anchor !== void 0 && !["center", "top", "right", "bottom", "left"].includes(input.anchor)) {
4284
+ fail("invalid-motion", `${path}.anchor`, "is not supported");
4285
+ }
4286
+ bounded(input.speed, `${path}.speed`, 1, 1e3);
4287
+ bounded(input.arrivalRadius, `${path}.arrivalRadius`, 0, 1e3);
4288
+ break;
4289
+ case "jump-to":
4290
+ closed3(input, ["type", "x", "y", "duration", "height"], path);
4291
+ bounded(input.x, `${path}.x`, -1e5, 1e5);
4292
+ bounded(input.y, `${path}.y`, -1e5, 1e5);
4293
+ bounded(input.duration, `${path}.duration`, 100, 1e4);
4294
+ bounded(input.height, `${path}.height`, 0, 2e3);
4295
+ break;
4296
+ case "svg-path": {
4297
+ closed3(input, ["type", "path", "duration", "loop", "relative"], path);
4298
+ if (typeof input.path !== "string" || input.path.length < 1 || input.path.length > 4096 || /[\0\r\n]/.test(input.path)) {
4299
+ fail("invalid-path", `${path}.path`, "must be bounded SVG path data");
4300
+ }
4301
+ if (context.validatePath && !context.validatePath(input.path)) {
4302
+ fail("invalid-path", `${path}.path`, "must be valid SVG path data");
4303
+ }
4304
+ bounded(input.duration, `${path}.duration`, 100, 6e4);
4305
+ for (const field of ["loop", "relative"]) {
4306
+ if (input[field] !== void 0 && typeof input[field] !== "boolean") {
4307
+ fail("invalid-motion", `${path}.${field}`, "must be boolean");
4308
+ }
4309
+ }
4310
+ break;
4311
+ }
4312
+ default:
4313
+ fail("invalid-motion", `${path}.type`, "is not a supported motion type");
4018
4314
  }
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
- });
4315
+ return freeze({ ...input });
4026
4316
  }
4027
4317
  function compileSurface(input, path, context, allowPayload) {
4028
4318
  object(input, path);
@@ -4363,7 +4653,25 @@
4363
4653
  thresholds: [...thresholds].sort((left, right) => left - right)
4364
4654
  };
4365
4655
  }
4366
- function createDefaultPlan(locomotion) {
4656
+ function createPresetPlan(preset, locomotion) {
4657
+ const state = locomotion ? { capability: "locomotion" } : { state: "idle" };
4658
+ if (preset === "still") {
4659
+ return {
4660
+ baseline: {
4661
+ channels: ["state"],
4662
+ state: { state: "idle" }
4663
+ }
4664
+ };
4665
+ }
4666
+ if (preset === "bottom-patrol" || preset === "viewport-roam") {
4667
+ return {
4668
+ baseline: {
4669
+ channels: ["motion", "state"],
4670
+ state,
4671
+ motion: preset === "bottom-patrol" ? { type: "horizontal-patrol", speed: DEFAULT_SPEED } : { type: "viewport-traverse", speed: DEFAULT_SPEED }
4672
+ }
4673
+ };
4674
+ }
4367
4675
  return {
4368
4676
  baseline: {
4369
4677
  channels: ["state"],
@@ -4389,6 +4697,12 @@
4389
4697
  #eventState = createPlanEventState();
4390
4698
  #activeChannels = /* @__PURE__ */ new Map();
4391
4699
  #activeSurfaces = /* @__PURE__ */ new Map();
4700
+ #patrolDirections = /* @__PURE__ */ new WeakMap();
4701
+ #traversePhases = /* @__PURE__ */ new WeakMap();
4702
+ #jumpOwner;
4703
+ #jumpState;
4704
+ #pathOwner;
4705
+ #pathState;
4392
4706
  #activeInterrupts = [];
4393
4707
  #surfaceVersion = 0;
4394
4708
  #lastEventStatus;
@@ -4476,24 +4790,81 @@
4476
4790
  if (state?.kind === "capability") {
4477
4791
  output.capability = state.name;
4478
4792
  }
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
- }
4793
+ const motionOwner = channels.get("motion");
4794
+ const motion = motionOwner?.motion;
4795
+ const settleCapability = () => {
4796
+ if (output.capability !== "locomotion") return;
4797
+ delete output.capability;
4798
+ const baseline = this.#baselineState();
4799
+ if (!output.state && baseline) output.state = baseline;
4800
+ };
4801
+ if (motion?.type !== "jump-to") {
4802
+ this.#jumpOwner = void 0;
4803
+ this.#jumpState = void 0;
4804
+ }
4805
+ if (motion?.type !== "svg-path") {
4806
+ this.#pathOwner = void 0;
4807
+ this.#pathState = void 0;
4808
+ }
4809
+ if (motion?.type === "horizontal-patrol" && world.characterSize) {
4810
+ const [direction, request] = evaluateHorizontalPatrol(
4811
+ motion,
4812
+ world,
4813
+ this.#patrolDirections.get(motionOwner) ?? true
4814
+ );
4815
+ this.#patrolDirections.set(motionOwner, direction);
4816
+ if (request) output.motion = request;
4817
+ } else if (motion?.type === "viewport-traverse" && world.characterSize) {
4818
+ const [phase, request] = evaluateViewportTraverse(
4819
+ motion,
4820
+ world,
4821
+ this.#traversePhases.get(motionOwner)
4822
+ );
4823
+ this.#traversePhases.set(motionOwner, phase);
4824
+ if (request) output.motion = request;
4825
+ } else if (motion?.type === "follow-pointer") {
4826
+ const request = world.pointer ? motionToward(
4827
+ motion,
4828
+ world.position,
4829
+ world.pointer.x,
4830
+ world.pointer.y,
4831
+ motion.arrivalRadius,
4832
+ world
4833
+ ) : void 0;
4834
+ if (request) output.motion = request;
4835
+ else settleCapability();
4836
+ } else if (motion?.type === "move-to") {
4837
+ const request = motionToward(
4838
+ motion,
4839
+ world.position,
4840
+ motion.x,
4841
+ motion.y,
4842
+ motion.arrivalRadius,
4843
+ world
4844
+ );
4845
+ if (request) output.motion = request;
4846
+ else settleCapability();
4847
+ } else if (motion?.type === "move-to-target") {
4848
+ const target = world.targets?.[motion.target];
4849
+ const point = target && world.characterSize ? targetPoint(target, motion.anchor, world.characterSize) : void 0;
4850
+ const request = point ? motionToward(
4851
+ motion,
4852
+ world.position,
4853
+ point.x,
4854
+ point.y,
4855
+ motion.arrivalRadius,
4856
+ world
4857
+ ) : void 0;
4858
+ if (request) output.motion = { ...request, trackTarget: true };
4859
+ else settleCapability();
4860
+ } else if (motion?.type === "jump-to") {
4861
+ const request = this.#evaluateJump(motionOwner, motion, world);
4862
+ if (request) output.motion = request;
4863
+ else settleCapability();
4864
+ } else if (motion?.type === "svg-path") {
4865
+ const request = this.#evaluatePath(motionOwner, motion, world);
4866
+ if (request) output.motion = request;
4867
+ else settleCapability();
4497
4868
  }
4498
4869
  const selected = /* @__PURE__ */ new Map();
4499
4870
  for (const surface of this.#plan.baseline.surfaces) {
@@ -4532,6 +4903,12 @@
4532
4903
  this.#activeChannels.clear();
4533
4904
  this.#activeSurfaces.clear();
4534
4905
  this.#activeInterrupts = [];
4906
+ this.#patrolDirections = /* @__PURE__ */ new WeakMap();
4907
+ this.#traversePhases = /* @__PURE__ */ new WeakMap();
4908
+ this.#jumpOwner = void 0;
4909
+ this.#jumpState = void 0;
4910
+ this.#pathOwner = void 0;
4911
+ this.#pathState = void 0;
4535
4912
  this.#eventState.ordering.clear();
4536
4913
  this.#eventState.sessions.clear();
4537
4914
  this.#lastEventStatus = void 0;
@@ -4557,17 +4934,75 @@
4557
4934
  const state = this.#plan.baseline.state;
4558
4935
  return state?.kind === "state" ? state.name : void 0;
4559
4936
  }
4937
+ #evaluateJump(owner, effect, world) {
4938
+ if (this.#jumpOwner !== owner || !this.#jumpState) {
4939
+ this.#jumpOwner = owner;
4940
+ this.#jumpState = {
4941
+ startedAt: world.now,
4942
+ from: { ...world.position }
4943
+ };
4944
+ }
4945
+ const duration = effect.duration ?? 800;
4946
+ const progress = Math.max(
4947
+ 0,
4948
+ Math.min(1, (world.now - this.#jumpState.startedAt) / duration)
4949
+ );
4950
+ const target = boundedPoint(effect, world);
4951
+ const desired = {
4952
+ x: this.#jumpState.from.x + (target.x - this.#jumpState.from.x) * progress,
4953
+ y: this.#jumpState.from.y + (target.y - this.#jumpState.from.y) * progress
4954
+ };
4955
+ const lift = progress === 0 || progress === 1 ? 0 : Math.sin(Math.PI * progress) * (effect.height ?? world.characterSize?.height ?? 32);
4956
+ const request = directMotion(world.position, desired, lift);
4957
+ if (progress < 1 || request.maxDistance) return request;
4958
+ return;
4959
+ }
4960
+ #evaluatePath(owner, effect, world) {
4961
+ const sample = world.samplePath;
4962
+ if (!sample) return;
4963
+ const first = sample(effect.path, 0);
4964
+ if (!first) return;
4965
+ if (this.#pathOwner !== owner || !this.#pathState) {
4966
+ this.#pathOwner = owner;
4967
+ this.#pathState = {
4968
+ startedAt: world.now,
4969
+ offset: effect.relative === false ? { x: 0, y: 0 } : {
4970
+ x: world.position.x - first.x,
4971
+ y: world.position.y - first.y
4972
+ }
4973
+ };
4974
+ }
4975
+ const duration = effect.duration ?? 4e3;
4976
+ const elapsed = Math.max(0, world.now - this.#pathState.startedAt);
4977
+ const complete = effect.loop !== true && elapsed >= duration;
4978
+ const progress = effect.loop ? elapsed % duration / duration : Math.min(1, elapsed / duration);
4979
+ const point = sample(effect.path, progress);
4980
+ if (!point) return;
4981
+ const desired = {
4982
+ x: point.x + this.#pathState.offset.x,
4983
+ y: point.y + this.#pathState.offset.y
4984
+ };
4985
+ const request = directMotion(world.position, boundedPoint(desired, world));
4986
+ if (!complete || request.maxDistance) return request;
4987
+ return;
4988
+ }
4560
4989
  #isContinuous(rule, world) {
4561
4990
  const condition = rule.when;
4562
4991
  if (condition.source !== "browser") return false;
4563
4992
  if (condition.event === "pointer.move") {
4564
4993
  if (!world.pointer) return false;
4565
4994
  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);
4995
+ if (!motion || motion.type !== "follow-pointer") return true;
4996
+ return Boolean(
4997
+ motionToward(
4998
+ motion,
4999
+ world.position,
5000
+ world.pointer.x,
5001
+ world.pointer.y,
5002
+ motion.arrivalRadius,
5003
+ world
5004
+ )
5005
+ );
4571
5006
  }
4572
5007
  if (condition.event !== "section.visibility") return false;
4573
5008
  const section = world.sections?.[condition.selector];
@@ -4610,6 +5045,128 @@
4610
5045
  function completionSource(name) {
4611
5046
  return browserCompletionEvent(name) ? "browser" : "application";
4612
5047
  }
5048
+ function evaluateHorizontalPatrol(effect, world, direction) {
5049
+ const halfWidth = world.characterSize.width / 2;
5050
+ const inset = effect.edgeInset ?? 0;
5051
+ const left = Math.min(world.viewport.width / 2, halfWidth + inset);
5052
+ const right = world.viewport.width - left;
5053
+ const halfHeight = world.characterSize.height / 2;
5054
+ const y = Math.max(halfHeight, world.viewport.height - halfHeight);
5055
+ let request = motionToward(
5056
+ effect,
5057
+ world.position,
5058
+ direction ? right : left,
5059
+ y
5060
+ );
5061
+ if (!request) {
5062
+ direction = !direction;
5063
+ request = motionToward(effect, world.position, direction ? right : left, y);
5064
+ }
5065
+ return [direction, request];
5066
+ }
5067
+ function evaluateViewportTraverse(effect, world, phase) {
5068
+ const inset = effect.edgeInset ?? 0;
5069
+ const halfWidth = world.characterSize.width / 2;
5070
+ const halfHeight = world.characterSize.height / 2;
5071
+ const left = Math.min(world.viewport.width / 2, halfWidth + inset);
5072
+ const right = Math.max(left, world.viewport.width - left);
5073
+ const top = Math.min(world.viewport.height / 2, halfHeight + inset);
5074
+ const bottom = Math.max(top, world.viewport.height - halfHeight - inset);
5075
+ const points = [
5076
+ { x: left, y: bottom },
5077
+ { x: right, y: bottom },
5078
+ { x: right, y: top },
5079
+ { x: left, y: top }
5080
+ ];
5081
+ if (phase === void 0) {
5082
+ phase = points.reduce(
5083
+ (nearest, point, index) => Math.hypot(point.x - world.position.x, point.y - world.position.y) < Math.hypot(
5084
+ points[nearest].x - world.position.x,
5085
+ points[nearest].y - world.position.y
5086
+ ) ? index : nearest,
5087
+ 0
5088
+ );
5089
+ }
5090
+ let target = points[phase];
5091
+ let request = motionToward(effect, world.position, target.x, target.y);
5092
+ if (!request) {
5093
+ const direction = effect.clockwise === false ? -1 : 1;
5094
+ phase = (phase + direction + points.length) % points.length;
5095
+ target = points[phase];
5096
+ request = motionToward(effect, world.position, target.x, target.y);
5097
+ }
5098
+ return [phase, request];
5099
+ }
5100
+ function targetPoint(target, anchor = "center", character) {
5101
+ const center = {
5102
+ x: target.left + target.width / 2,
5103
+ y: target.top + target.height / 2
5104
+ };
5105
+ switch (anchor) {
5106
+ case "top":
5107
+ return { x: center.x, y: target.top - character.height / 2 };
5108
+ case "right":
5109
+ return { x: target.right + character.width / 2, y: center.y };
5110
+ case "bottom":
5111
+ return { x: center.x, y: target.bottom + character.height / 2 };
5112
+ case "left":
5113
+ return { x: target.left - character.width / 2, y: center.y };
5114
+ default:
5115
+ return center;
5116
+ }
5117
+ }
5118
+ function boundedPoint(point, world) {
5119
+ const size = world.characterSize;
5120
+ if (!size) return { x: point.x, y: point.y };
5121
+ return {
5122
+ x: Math.max(
5123
+ size.width / 2,
5124
+ Math.min(world.viewport.width - size.width / 2, point.x)
5125
+ ),
5126
+ y: Math.max(
5127
+ size.height / 2,
5128
+ Math.min(world.viewport.height - size.height / 2, point.y)
5129
+ )
5130
+ };
5131
+ }
5132
+ function directMotion(position, target, lift = 0) {
5133
+ const dx = target.x - position.x;
5134
+ const dy = target.y - position.y;
5135
+ const distance = Math.hypot(dx, dy);
5136
+ return {
5137
+ x: distance ? dx / distance : 0,
5138
+ y: distance ? dy / distance : 0,
5139
+ speed: 0,
5140
+ maxDistance: distance,
5141
+ lift,
5142
+ direct: true
5143
+ };
5144
+ }
5145
+ function motionToward(effect, position, x, y, arrivalRadius = 0, bounds) {
5146
+ const size = bounds?.characterSize;
5147
+ const viewport = bounds?.viewport;
5148
+ if (size && viewport) {
5149
+ const pointerX = x;
5150
+ const pointerY = y;
5151
+ x = Math.max(size.width / 2, Math.min(viewport.width - size.width / 2, x));
5152
+ y = Math.max(
5153
+ size.height / 2,
5154
+ Math.min(viewport.height - size.height / 2, y)
5155
+ );
5156
+ if (x !== pointerX || y !== pointerY) arrivalRadius = 0;
5157
+ }
5158
+ const dx = x - position.x;
5159
+ const dy = y - position.y;
5160
+ const distance = Math.hypot(dx, dy);
5161
+ const maxDistance = distance - arrivalRadius;
5162
+ if (maxDistance <= 4) return;
5163
+ return {
5164
+ x: dx / distance,
5165
+ y: dy / distance,
5166
+ speed: effect.speed ?? DEFAULT_SPEED,
5167
+ maxDistance
5168
+ };
5169
+ }
4613
5170
 
4614
5171
  // packages/runtime/src/configuration-validation.ts
4615
5172
  var collectConfigurationFaults = !compactRuntimeDiagnostics;
@@ -4621,6 +5178,10 @@
4621
5178
  "atlasUrl",
4622
5179
  "styles",
4623
5180
  "plan",
5181
+ "preset",
5182
+ "targets",
5183
+ "interaction",
5184
+ "indicator",
4624
5185
  "content",
4625
5186
  "bindings",
4626
5187
  "theme",
@@ -4652,6 +5213,14 @@
4652
5213
  if (options.format !== void 0 && options.format !== 1) {
4653
5214
  reportConfigurationFault("unsupported-format", "$.format", fault);
4654
5215
  }
5216
+ if (options.plan !== void 0 && options.preset !== void 0) {
5217
+ reportConfigurationFault("incompatible-behavior", "$.preset", fault);
5218
+ }
5219
+ if (options.preset !== void 0 && !["companion", "still", "bottom-patrol", "viewport-roam"].includes(
5220
+ options.preset
5221
+ )) {
5222
+ reportConfigurationFault("invalid-preset", "$.preset", fault);
5223
+ }
4655
5224
  if (options.character !== void 0) {
4656
5225
  if (typeof options.character !== "string" || !NAME_PATTERN.test(options.character)) {
4657
5226
  reportConfigurationFault("invalid-character", "$.character", fault);
@@ -4737,6 +5306,12 @@
4737
5306
  if (collectConfigurationFaults) content = {};
4738
5307
  }
4739
5308
  validateBindings(options.bindings, content, fault);
5309
+ validateTargets(options.targets, fault);
5310
+ validateInteraction(options.interaction, options.targets, fault);
5311
+ validateIndicator(options.indicator, fault);
5312
+ if (options.interaction === false && options.indicator !== void 0) {
5313
+ reportConfigurationFault("incompatible-indicator", "$.indicator", fault);
5314
+ }
4740
5315
  validateTheme(options.theme, fault);
4741
5316
  validateDiagnostics(options.diagnostics, fault);
4742
5317
  validateAccessibility(options.accessibility, fault);
@@ -4747,6 +5322,201 @@
4747
5322
  }
4748
5323
  return content;
4749
5324
  }
5325
+ function validateInteraction(interaction, targets, fault) {
5326
+ if (interaction === void 0 || interaction === false) return;
5327
+ const checked = shape(
5328
+ interaction,
5329
+ "invalid-interaction",
5330
+ "$.interaction",
5331
+ [
5332
+ "press",
5333
+ "pressEvent",
5334
+ "drag",
5335
+ "throw",
5336
+ "dragState",
5337
+ "riseState",
5338
+ "fallState",
5339
+ "landState",
5340
+ "gravity",
5341
+ "maxThrowSpeed",
5342
+ "bounce",
5343
+ "floorInset",
5344
+ "label",
5345
+ "contentInitiallyHidden",
5346
+ "clearIndicatorOnPress",
5347
+ "catchTarget",
5348
+ "catchAnchor",
5349
+ "catchMargin",
5350
+ "catchEvent"
5351
+ ],
5352
+ fault
5353
+ );
5354
+ if (collectConfigurationFaults && !checked) return;
5355
+ const value = checked;
5356
+ if (value.press !== void 0 && ![
5357
+ "toggle-content",
5358
+ "show-content",
5359
+ "hide-content",
5360
+ "emit",
5361
+ "none"
5362
+ ].includes(value.press)) {
5363
+ reportConfigurationFault(
5364
+ "invalid-interaction",
5365
+ "$.interaction.press",
5366
+ fault
5367
+ );
5368
+ }
5369
+ if (value.pressEvent !== void 0 && (typeof value.pressEvent !== "string" || !EVENT_NAME_PATTERN.test(value.pressEvent))) {
5370
+ reportConfigurationFault(
5371
+ "invalid-interaction-event",
5372
+ "$.interaction.pressEvent",
5373
+ fault
5374
+ );
5375
+ }
5376
+ if (value.press === "emit" && value.pressEvent === void 0) {
5377
+ reportConfigurationFault(
5378
+ "missing-interaction-event",
5379
+ "$.interaction.pressEvent",
5380
+ fault
5381
+ );
5382
+ }
5383
+ if (value.pressEvent !== void 0 && value.press !== void 0 && value.press !== "emit") {
5384
+ reportConfigurationFault(
5385
+ "incompatible-interaction-event",
5386
+ "$.interaction.pressEvent",
5387
+ fault
5388
+ );
5389
+ }
5390
+ for (const field of [
5391
+ "drag",
5392
+ "throw",
5393
+ "contentInitiallyHidden",
5394
+ "clearIndicatorOnPress"
5395
+ ]) {
5396
+ if (value[field] !== void 0 && typeof value[field] !== "boolean") {
5397
+ reportConfigurationFault(
5398
+ "invalid-interaction",
5399
+ `$.interaction.${field}`,
5400
+ fault
5401
+ );
5402
+ }
5403
+ }
5404
+ for (const field of [
5405
+ "dragState",
5406
+ "riseState",
5407
+ "fallState",
5408
+ "landState"
5409
+ ]) {
5410
+ if (value[field] !== void 0 && (typeof value[field] !== "string" || !STATE_NAME_PATTERN.test(value[field]))) {
5411
+ reportConfigurationFault(
5412
+ "invalid-interaction-state",
5413
+ `$.interaction.${field}`,
5414
+ fault
5415
+ );
5416
+ }
5417
+ }
5418
+ for (const [field, minimum, maximum] of [
5419
+ ["gravity", 0, 1e4],
5420
+ ["maxThrowSpeed", 0, 1e4],
5421
+ ["bounce", 0, 1],
5422
+ ["floorInset", 0, 1e3],
5423
+ ["catchMargin", 0, 1e3]
5424
+ ]) {
5425
+ const item = value[field];
5426
+ if (item !== void 0 && (typeof item !== "number" || !Number.isFinite(item) || item < minimum || item > maximum)) {
5427
+ reportConfigurationFault(
5428
+ "invalid-interaction-range",
5429
+ `$.interaction.${field}`,
5430
+ fault
5431
+ );
5432
+ }
5433
+ }
5434
+ if (value.label !== void 0 && !text2(value.label, 120)) {
5435
+ reportConfigurationFault(
5436
+ "invalid-interaction-label",
5437
+ "$.interaction.label",
5438
+ fault
5439
+ );
5440
+ }
5441
+ if (value.catchTarget !== void 0 && (typeof value.catchTarget !== "string" || !NAME_PATTERN.test(value.catchTarget) || !Object.hasOwn(targets ?? {}, value.catchTarget))) {
5442
+ reportConfigurationFault(
5443
+ "invalid-catch-target",
5444
+ "$.interaction.catchTarget",
5445
+ fault
5446
+ );
5447
+ }
5448
+ if (value.catchAnchor !== void 0 && !["center", "top", "right", "bottom", "left"].includes(
5449
+ value.catchAnchor
5450
+ )) {
5451
+ reportConfigurationFault(
5452
+ "invalid-catch-anchor",
5453
+ "$.interaction.catchAnchor",
5454
+ fault
5455
+ );
5456
+ }
5457
+ if (value.catchEvent !== void 0 && (typeof value.catchEvent !== "string" || !EVENT_NAME_PATTERN.test(value.catchEvent))) {
5458
+ reportConfigurationFault(
5459
+ "invalid-interaction-event",
5460
+ "$.interaction.catchEvent",
5461
+ fault
5462
+ );
5463
+ }
5464
+ }
5465
+ function validateIndicator(indicator, fault) {
5466
+ if (indicator === void 0) return;
5467
+ const checked = shape(
5468
+ indicator,
5469
+ "invalid-indicator",
5470
+ "$.indicator",
5471
+ ["kind", "count", "label", "color", "visible"],
5472
+ fault
5473
+ );
5474
+ if (collectConfigurationFaults && !checked) return;
5475
+ const value = checked;
5476
+ if (value.kind !== void 0 && value.kind !== "dot" && value.kind !== "count") {
5477
+ reportConfigurationFault("invalid-indicator", "$.indicator.kind", fault);
5478
+ }
5479
+ if (!text2(value.label, 120)) {
5480
+ reportConfigurationFault("invalid-indicator", "$.indicator.label", fault);
5481
+ }
5482
+ if (value.count !== void 0 && (typeof value.count !== "number" || !Number.isInteger(value.count) || value.count < 0 || value.count > 999)) {
5483
+ reportConfigurationFault("invalid-indicator", "$.indicator.count", fault);
5484
+ }
5485
+ if (value.color !== void 0 && !isSurfaceColor(value.color)) {
5486
+ reportConfigurationFault("invalid-indicator", "$.indicator.color", fault);
5487
+ }
5488
+ if (value.visible !== void 0 && typeof value.visible !== "boolean") {
5489
+ reportConfigurationFault("invalid-indicator", "$.indicator.visible", fault);
5490
+ }
5491
+ }
5492
+ function validateTargets(targets, fault) {
5493
+ if (targets === void 0) return;
5494
+ const checked = shape(
5495
+ targets,
5496
+ "invalid-targets",
5497
+ "$.targets",
5498
+ void 0,
5499
+ fault
5500
+ );
5501
+ if (collectConfigurationFaults && !checked) return;
5502
+ const entries = Object.entries(checked);
5503
+ if (entries.length > 16) {
5504
+ reportConfigurationFault("target-limit", "$.targets", fault);
5505
+ }
5506
+ for (const [id2, selector2] of entries) {
5507
+ if (!NAME_PATTERN.test(id2)) {
5508
+ reportConfigurationFault("invalid-target", `$.targets.${id2}`, fault, id2);
5509
+ }
5510
+ if (typeof selector2 !== "string" || selector2.length < 1 || selector2.length > 256 || /[\0\r\n]/.test(selector2)) {
5511
+ reportConfigurationFault(
5512
+ "invalid-target-selector",
5513
+ `$.targets.${id2}`,
5514
+ fault,
5515
+ id2
5516
+ );
5517
+ }
5518
+ }
5519
+ }
4750
5520
  function validRuntimeName(value) {
4751
5521
  return value === void 0 || typeof value === "boolean" || text2(value, 80);
4752
5522
  }
@@ -4975,7 +5745,8 @@
4975
5745
  if (options.plan !== void 0) {
4976
5746
  try {
4977
5747
  compilePlan(options.plan, {
4978
- contentIds: new Set(Object.keys(content))
5748
+ contentIds: new Set(Object.keys(content)),
5749
+ targets: new Set(Object.keys(options.targets ?? {}))
4979
5750
  });
4980
5751
  } catch (cause) {
4981
5752
  fail2(
@@ -5056,6 +5827,40 @@
5056
5827
  return `${detail ?? field} must be a function`;
5057
5828
  case "invalid-plan":
5058
5829
  return detail ?? "Plan is invalid";
5830
+ case "incompatible-behavior":
5831
+ return "Choose preset or plan, not both";
5832
+ case "invalid-preset":
5833
+ return "preset must be companion, still, bottom-patrol, or viewport-roam";
5834
+ case "invalid-targets":
5835
+ return "targets must be an object of target IDs and CSS selectors";
5836
+ case "target-limit":
5837
+ return "targets supports at most 16 entries";
5838
+ case "invalid-target":
5839
+ return "target IDs must be bounded lowercase IDs";
5840
+ case "invalid-target-selector":
5841
+ return "target selectors must be bounded CSS selector text";
5842
+ case "invalid-interaction":
5843
+ return "interaction contains an unsupported value";
5844
+ case "invalid-interaction-event":
5845
+ return "interaction events must be bounded application Event names";
5846
+ case "missing-interaction-event":
5847
+ return "pressEvent is required when press is emit";
5848
+ case "incompatible-interaction-event":
5849
+ return "pressEvent can be used only with the emit press action";
5850
+ case "invalid-interaction-state":
5851
+ return "interaction states must be valid Pack State names";
5852
+ case "invalid-interaction-range":
5853
+ return "interaction physics value is outside its supported range";
5854
+ case "invalid-interaction-label":
5855
+ return "interaction label must be short plain text";
5856
+ case "invalid-catch-target":
5857
+ return "catchTarget must reference a configured target";
5858
+ case "invalid-catch-anchor":
5859
+ return "catchAnchor is not supported";
5860
+ case "invalid-indicator":
5861
+ return "indicator must include a label and valid dot or count settings";
5862
+ case "incompatible-indicator":
5863
+ return "indicator requires character interaction";
5059
5864
  default:
5060
5865
  return detail ?? code;
5061
5866
  }
@@ -5168,21 +5973,26 @@
5168
5973
  delete output.state;
5169
5974
  }
5170
5975
  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
- }
5976
+ if (motion?.type === "horizontal-patrol") {
5977
+ const [direction, request] = evaluateHorizontalPatrol(
5978
+ motion,
5979
+ world,
5980
+ item.patrolDirection
5981
+ );
5982
+ item.patrolDirection = direction;
5983
+ if (request) output.motion = request;
5984
+ else delete output.motion;
5985
+ } else if (motion?.type === "follow-pointer" && world.pointer && world.position) {
5986
+ const request = motionToward(
5987
+ motion,
5988
+ world.position,
5989
+ world.pointer.x,
5990
+ world.pointer.y,
5991
+ motion.arrivalRadius,
5992
+ world
5993
+ );
5994
+ if (request) output.motion = request;
5995
+ else delete output.motion;
5186
5996
  }
5187
5997
  for (const surface of item.effect.surfaces) {
5188
5998
  surfaces.set(
@@ -5249,6 +6059,7 @@
5249
6059
  startedAt: 0,
5250
6060
  deadline: 0,
5251
6061
  status: "rejected",
6062
+ patrolDirection: true,
5252
6063
  handle,
5253
6064
  finish
5254
6065
  });
@@ -5556,6 +6367,13 @@
5556
6367
  this.host.setAttribute("aria-hidden", "true");
5557
6368
  }
5558
6369
  };
6370
+ var createAtlasRenderer = (options) => new AtlasRenderer(
6371
+ options.document,
6372
+ options.pack,
6373
+ options.atlasUrl,
6374
+ options.scale,
6375
+ options.styles
6376
+ );
5559
6377
 
5560
6378
  // packages/runtime/src/resolver.ts
5561
6379
  var ALIASES = {
@@ -5799,6 +6617,142 @@
5799
6617
  }
5800
6618
  }
5801
6619
 
6620
+ // packages/runtime/src/path-sampler.ts
6621
+ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
6622
+ var SvgPathSampler = class {
6623
+ #document;
6624
+ #cache = /* @__PURE__ */ new Map();
6625
+ constructor(document) {
6626
+ this.#document = document;
6627
+ }
6628
+ validate(path) {
6629
+ return this.#geometry(path) !== void 0;
6630
+ }
6631
+ sample(path, progress) {
6632
+ const geometry = this.#geometry(path);
6633
+ if (!geometry) return;
6634
+ const bounded2 = Math.max(0, Math.min(1, progress));
6635
+ try {
6636
+ const point = geometry.element.getPointAtLength(
6637
+ geometry.length * bounded2
6638
+ );
6639
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
6640
+ return { x: point.x, y: point.y };
6641
+ } catch {
6642
+ return;
6643
+ }
6644
+ }
6645
+ #geometry(path) {
6646
+ const cached = this.#cache.get(path);
6647
+ if (cached) return cached;
6648
+ if (this.#cache.size >= 16) return;
6649
+ const element = this.#document.createElementNS(
6650
+ SVG_NAMESPACE,
6651
+ "path"
6652
+ );
6653
+ element.setAttribute("d", path);
6654
+ try {
6655
+ const length = element.getTotalLength();
6656
+ if (!Number.isFinite(length) || length <= 0) return;
6657
+ const geometry = Object.freeze({ element, length });
6658
+ this.#cache.set(path, geometry);
6659
+ return geometry;
6660
+ } catch {
6661
+ return;
6662
+ }
6663
+ }
6664
+ };
6665
+
6666
+ // packages/runtime/src/targets.ts
6667
+ var TargetTracker = class {
6668
+ #document;
6669
+ #window;
6670
+ #selectors;
6671
+ #elements = /* @__PURE__ */ new Map();
6672
+ #wake;
6673
+ #resizeObserver;
6674
+ #mutationObserver;
6675
+ #snapshot = Object.freeze({});
6676
+ #dirty = true;
6677
+ #destroyed = false;
6678
+ constructor(document, window, selectors, wake) {
6679
+ this.#document = document;
6680
+ this.#window = window;
6681
+ this.#selectors = selectors;
6682
+ this.#wake = wake;
6683
+ const browserWindow = window;
6684
+ this.#resizeObserver = browserWindow.ResizeObserver ? new browserWindow.ResizeObserver(() => this.refresh()) : void 0;
6685
+ this.#mutationObserver = browserWindow.MutationObserver ? new browserWindow.MutationObserver(() => this.refresh()) : void 0;
6686
+ this.#mutationObserver?.observe(document.documentElement, {
6687
+ attributes: true,
6688
+ attributeFilter: ["class", "id"],
6689
+ childList: true,
6690
+ subtree: true
6691
+ });
6692
+ window.addEventListener("resize", this.#markDirty, { passive: true });
6693
+ window.addEventListener("scroll", this.#markDirty, {
6694
+ capture: true,
6695
+ passive: true
6696
+ });
6697
+ }
6698
+ snapshot(force = false) {
6699
+ if (this.#destroyed) return Object.freeze({});
6700
+ if (force) this.#dirty = true;
6701
+ if (!this.#dirty) return this.#snapshot;
6702
+ const output = /* @__PURE__ */ Object.create(null);
6703
+ for (const [id2, selector2] of Object.entries(this.#selectors)) {
6704
+ let element = this.#elements.get(id2);
6705
+ if (!element?.isConnected) {
6706
+ if (element) this.#resizeObserver?.unobserve(element);
6707
+ element = this.#document.querySelector(selector2);
6708
+ if (element) {
6709
+ this.#elements.set(id2, element);
6710
+ this.#resizeObserver?.observe(element);
6711
+ } else {
6712
+ this.#elements.delete(id2);
6713
+ }
6714
+ }
6715
+ if (!element) continue;
6716
+ const rect = element.getBoundingClientRect();
6717
+ if (!Number.isFinite(rect.left) || !Number.isFinite(rect.top) || !Number.isFinite(rect.width) || !Number.isFinite(rect.height)) {
6718
+ continue;
6719
+ }
6720
+ output[id2] = Object.freeze({
6721
+ left: rect.left,
6722
+ top: rect.top,
6723
+ right: rect.right,
6724
+ bottom: rect.bottom,
6725
+ width: rect.width,
6726
+ height: rect.height
6727
+ });
6728
+ }
6729
+ this.#snapshot = Object.freeze(output);
6730
+ this.#dirty = false;
6731
+ return this.#snapshot;
6732
+ }
6733
+ contains(id2, point, margin = 0) {
6734
+ const target = this.snapshot(true)[id2];
6735
+ return Boolean(
6736
+ target && point.x >= target.left - margin && point.x <= target.right + margin && point.y >= target.top - margin && point.y <= target.bottom + margin
6737
+ );
6738
+ }
6739
+ refresh = () => {
6740
+ if (this.#destroyed) return;
6741
+ this.#dirty = true;
6742
+ this.#wake();
6743
+ };
6744
+ destroy() {
6745
+ if (this.#destroyed) return;
6746
+ this.#destroyed = true;
6747
+ this.#resizeObserver?.disconnect();
6748
+ this.#mutationObserver?.disconnect();
6749
+ this.#window.removeEventListener("resize", this.#markDirty);
6750
+ this.#window.removeEventListener("scroll", this.#markDirty, true);
6751
+ this.#elements.clear();
6752
+ }
6753
+ #markDirty = () => this.refresh();
6754
+ };
6755
+
5802
6756
  // packages/runtime/src/visibility.ts
5803
6757
  var PREFERENCE_KEY = "peekling:site-preference";
5804
6758
  var SESSION_KEY = "peekling:hidden-session";
@@ -6157,6 +7111,10 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6157
7111
  var SUSPEND_PAGE = 2;
6158
7112
  var SUSPEND_SITE = 4;
6159
7113
  var MAX_TIMER_DELAY = 2147483647;
7114
+ var DEFAULT_GRAVITY = 1800;
7115
+ var DEFAULT_THROW_SPEED = 1800;
7116
+ var DEFAULT_BOUNCE = 0.35;
7117
+ var DEFAULT_FLOOR_INSET = 8;
6160
7118
  var EMPTY_CONTENT_SELECTIONS = Object.freeze([]);
6161
7119
  function browserSelectorIsValid(document, selector2) {
6162
7120
  try {
@@ -6220,10 +7178,21 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6220
7178
  }
6221
7179
  return input;
6222
7180
  }
7181
+ function checkedIndicator(input) {
7182
+ if (input === null) return;
7183
+ const value = snapshotConfiguration(input);
7184
+ 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") {
7185
+ throw new TypeError(
7186
+ runtimeMessage("indicator.invalid", "Indicator is invalid")
7187
+ );
7188
+ }
7189
+ return value;
7190
+ }
6223
7191
  var PeeklingRuntime = class _PeeklingRuntime {
6224
- static hatch(input) {
7192
+ static hatch(input, rendererFactory = createAtlasRenderer) {
6225
7193
  return new _PeeklingRuntime(
6226
- hatchOptions(input)
7194
+ hatchOptions(input),
7195
+ rendererFactory
6227
7196
  );
6228
7197
  }
6229
7198
  /** Package-internal presentation update used only by the Web Component. */
@@ -6259,7 +7228,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6259
7228
  #loaded;
6260
7229
  #renderer;
6261
7230
  #contentRenderer;
7231
+ #interactionController;
6262
7232
  #sections;
7233
+ #targets;
6263
7234
  #input;
6264
7235
  #raf = 0;
6265
7236
  #planTimer = 0;
@@ -6295,10 +7266,18 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6295
7266
  #instanceId;
6296
7267
  #diagnostics;
6297
7268
  #styles;
7269
+ #rendererFactory;
6298
7270
  #rejectedOverrideId = 0;
6299
7271
  #reportedNotReadyEvent = false;
6300
7272
  #reportedNotReadyOverride = false;
6301
- constructor(options = {}) {
7273
+ #trackTargets = false;
7274
+ #pathSampler;
7275
+ #indicator;
7276
+ #dragging = false;
7277
+ #throwing = false;
7278
+ #throwVelocity = { x: 0, y: 0 };
7279
+ #landUntil = 0;
7280
+ constructor(options = {}, rendererFactory = createAtlasRenderer) {
6302
7281
  options = snapshotConfiguration(options);
6303
7282
  const document = options.document ?? globalThis.document;
6304
7283
  const window = options.window ?? globalThis.window;
@@ -6308,7 +7287,10 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6308
7287
  );
6309
7288
  this.#document = document;
6310
7289
  this.#window = window;
7290
+ this.#pathSampler = new SvgPathSampler(document);
6311
7291
  this.#options = options;
7292
+ this.#indicator = options.indicator;
7293
+ this.#rendererFactory = rendererFactory;
6312
7294
  this.#now = window.performance?.now?.bind(window.performance) ?? Date.now;
6313
7295
  this.#instanceId = `instance:${Math.floor(this.#now())}:${Math.random().toString(36).slice(2, 8)}`;
6314
7296
  this.#diagnostics = new DiagnosticChannel({
@@ -6433,6 +7415,28 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6433
7415
  this.#setSuspended(SUSPEND_HOST, false);
6434
7416
  this.#notice("lifecycle.resumed", "info", "lifecycle");
6435
7417
  }
7418
+ refreshTargets() {
7419
+ this.#targets?.refresh();
7420
+ }
7421
+ setIndicator(indicator) {
7422
+ if (this.#destroyed) return;
7423
+ this.#indicator = checkedIndicator(indicator);
7424
+ this.#interactionController?.updateIndicator(this.#indicator);
7425
+ }
7426
+ setContentVisible(visible) {
7427
+ if (this.#destroyed || !this.#contentRenderer) return false;
7428
+ const expanded = this.#contentRenderer.setUserHidden(!visible);
7429
+ this.#interactionController?.setExpanded(expanded);
7430
+ this.#wake();
7431
+ return expanded;
7432
+ }
7433
+ toggleContent() {
7434
+ if (this.#destroyed || !this.#contentRenderer) return false;
7435
+ const expanded = this.#contentRenderer.toggleUserHidden();
7436
+ this.#interactionController?.setExpanded(expanded);
7437
+ this.#wake();
7438
+ return expanded;
7439
+ }
6436
7440
  destroy() {
6437
7441
  this.#destroy("destroyed");
6438
7442
  }
@@ -6446,7 +7450,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6446
7450
  this.#input?.destroy();
6447
7451
  this.#plan?.reset();
6448
7452
  this.#sections?.destroy();
7453
+ this.#targets?.destroy();
6449
7454
  this.#contentRenderer?.destroy();
7455
+ this.#interactionController?.destroy();
6450
7456
  this.#renderer?.destroy();
6451
7457
  this.#unregisterVisibility?.();
6452
7458
  this.#unregisterVisibility = void 0;
@@ -6476,10 +7482,20 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6476
7482
  async #initialize() {
6477
7483
  try {
6478
7484
  const validateSelector = (selector2) => browserSelectorIsValid(this.#document, selector2);
7485
+ const targetIds = new Set(Object.keys(this.#options.targets ?? {}));
7486
+ for (const selector2 of Object.values(this.#options.targets ?? {})) {
7487
+ if (!validateSelector(selector2)) {
7488
+ throw new TypeError(
7489
+ runtimeMessage("target.selector", "Target selector is invalid")
7490
+ );
7491
+ }
7492
+ }
6479
7493
  if (this.#options.plan !== void 0) {
6480
7494
  compilePlan(this.#options.plan, {
6481
7495
  contentIds: new Set(Object.keys(this.#content)),
6482
- validateSelector
7496
+ targets: targetIds,
7497
+ validateSelector,
7498
+ validatePath: (path) => this.#pathSampler.validate(path)
6483
7499
  });
6484
7500
  }
6485
7501
  const browserWindow = this.#window;
@@ -6547,12 +7563,14 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6547
7563
  locomotion ? ["locomotion"] : []
6548
7564
  );
6549
7565
  const compiled = compilePlan(
6550
- this.#options.plan ?? createDefaultPlan(locomotion),
7566
+ this.#options.plan ?? createPresetPlan(this.#options.preset ?? "companion", locomotion),
6551
7567
  {
6552
7568
  states: new Set(Object.keys(loaded.content.states)),
6553
7569
  capabilities,
6554
7570
  contentIds: new Set(Object.keys(this.#content)),
6555
- validateSelector
7571
+ targets: targetIds,
7572
+ validateSelector,
7573
+ validatePath: (path) => this.#pathSampler.validate(path)
6556
7574
  }
6557
7575
  );
6558
7576
  this.#plan = new PlanRuntime(compiled);
@@ -6560,7 +7578,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6560
7578
  {
6561
7579
  states: new Set(Object.keys(loaded.content.states)),
6562
7580
  capabilities,
6563
- contentIds: new Set(Object.keys(this.#content))
7581
+ contentIds: new Set(Object.keys(this.#content)),
7582
+ targets: targetIds,
7583
+ validatePath: (path) => this.#pathSampler.validate(path)
6564
7584
  },
6565
7585
  this.#now,
6566
7586
  (message) => this.#diagnose(message, "override.rejected", "warning", "override")
@@ -6588,21 +7608,45 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6588
7608
  (loaded.content.atlas.logicalWidth ?? loaded.content.atlas.cellWidth) * scale,
6589
7609
  (loaded.content.atlas.logicalHeight ?? loaded.content.atlas.cellHeight) * scale
6590
7610
  );
6591
- this.#renderer = new AtlasRenderer(
6592
- this.#document,
6593
- loaded.content,
6594
- loaded.atlasObjectUrl,
7611
+ this.#renderer = this.#rendererFactory({
7612
+ document: this.#document,
7613
+ pack: loaded.content,
7614
+ atlasUrl: loaded.atlasObjectUrl,
6595
7615
  scale,
6596
- this.#styles
6597
- );
7616
+ styles: this.#styles
7617
+ });
6598
7618
  if (Object.keys(this.#content).length || this.#displayName !== void 0) {
6599
7619
  this.#ensureContentRenderer();
6600
7620
  }
7621
+ if (this.#options.interaction !== false) {
7622
+ const interaction = this.#options.interaction ?? {};
7623
+ const pressAction = interaction.press ?? (interaction.pressEvent ? "emit" : "toggle-content");
7624
+ const controlsContent = Boolean(this.#contentRenderer) && (pressAction === "toggle-content" || pressAction === "show-content" || pressAction === "hide-content");
7625
+ if (interaction.contentInitiallyHidden) {
7626
+ this.#contentRenderer?.setUserHidden(true);
7627
+ }
7628
+ this.#interactionController = new CharacterInteractionController({
7629
+ document: this.#document,
7630
+ styles: this.#styles,
7631
+ label: interaction.label ?? `Interact with ${loaded.content.displayName || "Peekling"}`,
7632
+ draggable: interaction.drag !== false,
7633
+ ...controlsContent ? { expanded: !interaction.contentInitiallyHidden } : {},
7634
+ ...this.#indicator ? { indicator: this.#indicator } : {},
7635
+ onPress: () => this.#handleCharacterPress(),
7636
+ onDragStart: () => this.#startCharacterDrag(),
7637
+ onDragMove: (position, velocity) => this.#moveCharacterDrag(position, velocity),
7638
+ onDragEnd: (velocity, moved, pointer) => this.#endCharacterDrag(velocity, moved, pointer)
7639
+ });
7640
+ if (interaction.contentInitiallyHidden) {
7641
+ this.#interactionController.setExpanded(false);
7642
+ }
7643
+ }
6601
7644
  try {
6602
7645
  await Promise.all([
6603
7646
  this.#visibilityReady,
6604
7647
  this.#renderer.ready,
6605
- this.#contentRenderer?.ready
7648
+ this.#contentRenderer?.ready,
7649
+ this.#interactionController?.ready
6606
7650
  ]);
6607
7651
  } catch (cause) {
6608
7652
  this.#diagnoseStyleFailure(cause);
@@ -6610,6 +7654,14 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6610
7654
  }
6611
7655
  const requirements = compiledPlanSectionRequirements(compiled);
6612
7656
  this.#createInput();
7657
+ if (targetIds.size) {
7658
+ this.#targets = new TargetTracker(
7659
+ this.#document,
7660
+ this.#window,
7661
+ this.#options.targets,
7662
+ () => this.#wake()
7663
+ );
7664
+ }
6613
7665
  if (requirements.selectors.length) {
6614
7666
  this.#sections = new SectionTracker(
6615
7667
  this.#document,
@@ -6621,6 +7673,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6621
7673
  );
6622
7674
  }
6623
7675
  this.#renderer.setHidden(this.#siteHidden);
7676
+ this.#interactionController?.setHidden(this.#siteHidden);
6624
7677
  if (this.#suspensions) {
6625
7678
  this.#input?.setSuspended(true);
6626
7679
  this.#sections?.setSuspended(true);
@@ -6684,6 +7737,129 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6684
7737
  });
6685
7738
  return this.#contentRenderer;
6686
7739
  }
7740
+ #handleCharacterPress() {
7741
+ if (this.#destroyed) return;
7742
+ const interaction = this.#options.interaction || {};
7743
+ if (interaction.clearIndicatorOnPress === true && this.#indicator) {
7744
+ this.setIndicator(null);
7745
+ }
7746
+ const action = interaction.press ?? (interaction.pressEvent ? "emit" : "toggle-content");
7747
+ if (action === "emit") {
7748
+ if (interaction.pressEvent) this.emit(interaction.pressEvent);
7749
+ return;
7750
+ }
7751
+ if (action === "none") return;
7752
+ if (action === "show-content") this.setContentVisible(true);
7753
+ else if (action === "hide-content") this.setContentVisible(false);
7754
+ else this.toggleContent();
7755
+ }
7756
+ #startCharacterDrag() {
7757
+ if (this.#destroyed || this.#siteHidden) return;
7758
+ this.#dragging = true;
7759
+ this.#throwing = false;
7760
+ this.#landUntil = 0;
7761
+ this.#throwVelocity = { x: 0, y: 0 };
7762
+ this.#wake();
7763
+ }
7764
+ #moveCharacterDrag(position, velocity) {
7765
+ if (!this.#dragging || this.#destroyed) return;
7766
+ const size = this.#characterSize();
7767
+ this.#position = size ? clampPosition(
7768
+ position,
7769
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7770
+ size.width,
7771
+ size.height
7772
+ ) : { ...position };
7773
+ this.#throwVelocity = { ...velocity };
7774
+ this.#wake();
7775
+ }
7776
+ #endCharacterDrag(velocity, moved, pointer) {
7777
+ if (!this.#dragging || this.#destroyed) return;
7778
+ this.#dragging = false;
7779
+ if (!moved) {
7780
+ this.#throwVelocity = { x: 0, y: 0 };
7781
+ this.#wake();
7782
+ return;
7783
+ }
7784
+ const interaction = this.#options.interaction || {};
7785
+ if (interaction.catchTarget && this.#targets?.contains(
7786
+ interaction.catchTarget,
7787
+ pointer,
7788
+ interaction.catchMargin ?? 24
7789
+ ) && this.#catchCharacter(interaction.catchTarget)) {
7790
+ return;
7791
+ }
7792
+ if (this.#reducedMotion) {
7793
+ this.#landCharacter();
7794
+ return;
7795
+ }
7796
+ if (interaction.throw === false) {
7797
+ this.#throwVelocity = { x: 0, y: 0 };
7798
+ this.#wake();
7799
+ return;
7800
+ }
7801
+ const maximum = interaction.maxThrowSpeed ?? DEFAULT_THROW_SPEED;
7802
+ const magnitude = Math.hypot(velocity.x, velocity.y);
7803
+ const factor = magnitude > maximum && magnitude ? maximum / magnitude : 1;
7804
+ this.#throwVelocity = {
7805
+ x: velocity.x * factor,
7806
+ y: velocity.y * factor
7807
+ };
7808
+ this.#throwing = true;
7809
+ this.#wake();
7810
+ }
7811
+ #characterSize() {
7812
+ const loaded = this.#loaded?.content;
7813
+ if (!loaded) return;
7814
+ const scale = this.#options.scale ?? loaded.defaultScale;
7815
+ return {
7816
+ width: (loaded.atlas.logicalWidth ?? loaded.atlas.cellWidth) * scale,
7817
+ height: (loaded.atlas.logicalHeight ?? loaded.atlas.cellHeight) * scale
7818
+ };
7819
+ }
7820
+ #landCharacter() {
7821
+ const size = this.#characterSize();
7822
+ if (size) {
7823
+ const floorInset = (this.#options.interaction || {}).floorInset ?? DEFAULT_FLOOR_INSET;
7824
+ this.#position = clampPosition(
7825
+ {
7826
+ x: this.#position.x,
7827
+ y: this.#window.innerHeight - size.height / 2 - floorInset
7828
+ },
7829
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7830
+ size.width,
7831
+ size.height
7832
+ );
7833
+ }
7834
+ this.#dragging = false;
7835
+ this.#throwing = false;
7836
+ this.#throwVelocity = { x: 0, y: 0 };
7837
+ this.#landUntil = this.#now() + 360;
7838
+ this.#wake();
7839
+ }
7840
+ #catchCharacter(targetId) {
7841
+ const target = this.#targets?.snapshot(true)[targetId];
7842
+ const size = this.#characterSize();
7843
+ if (!target || !size) return false;
7844
+ const interaction = this.#options.interaction || {};
7845
+ const point = characterTargetPoint(
7846
+ target,
7847
+ interaction.catchAnchor ?? "center",
7848
+ size
7849
+ );
7850
+ this.#position = clampPosition(
7851
+ point,
7852
+ { width: this.#window.innerWidth, height: this.#window.innerHeight },
7853
+ size.width,
7854
+ size.height
7855
+ );
7856
+ this.#throwing = false;
7857
+ this.#throwVelocity = { x: 0, y: 0 };
7858
+ this.#landUntil = this.#now() + 360;
7859
+ if (interaction.catchEvent) this.emit(interaction.catchEvent);
7860
+ this.#wake();
7861
+ return true;
7862
+ }
6687
7863
  #diagnose(message, code = "runtime.notice", severity = "warning", phase = "internal", eventId, metadata, cause) {
6688
7864
  this.#diagnostics.emit({
6689
7865
  code,
@@ -6770,6 +7946,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6770
7946
  if (hidden === this.#siteHidden) return;
6771
7947
  this.#siteHidden = hidden;
6772
7948
  this.#renderer?.setHidden(hidden);
7949
+ this.#interactionController?.setHidden(hidden);
6773
7950
  if (this.#destroyed) return;
6774
7951
  if (hidden && this.#contentRenderer) {
6775
7952
  this.#contentRenderer.renderSurfaces(
@@ -6785,6 +7962,9 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6785
7962
  if (this.#destroyed) return;
6786
7963
  }
6787
7964
  if (hidden) {
7965
+ this.#dragging = false;
7966
+ this.#throwing = false;
7967
+ this.#throwVelocity = { x: 0, y: 0 };
6788
7968
  this.#overrides?.clear("dismissed");
6789
7969
  this.#setSuspended(SUSPEND_SITE, true);
6790
7970
  this.#notice("lifecycle.dismissed", "info", "lifecycle");
@@ -6880,8 +8060,13 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6880
8060
  const loaded = this.#loaded;
6881
8061
  this.#densityUpgrade = loaded.upgrade(required).then((changed) => {
6882
8062
  if (changed && !this.#destroyed && this.#loaded === loaded) {
6883
- this.#renderer?.swapAtlas(loaded.content, loaded.atlasObjectUrl);
6884
- this.#wake();
8063
+ const renderer = this.#renderer;
8064
+ if (!renderer) return;
8065
+ return Promise.resolve(
8066
+ renderer.swapAtlas(loaded.content, loaded.atlasObjectUrl)
8067
+ ).then(() => {
8068
+ if (!this.#destroyed && this.#loaded === loaded) this.#wake();
8069
+ });
6885
8070
  }
6886
8071
  }).catch((error) => {
6887
8072
  if (!this.#destroyed)
@@ -6953,10 +8138,13 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6953
8138
  pointer: input.pointer,
6954
8139
  position: this.#position,
6955
8140
  viewport,
8141
+ characterSize,
6956
8142
  lastActivityAt: input.lastActivityAt,
6957
8143
  reducedMotion: this.#reducedMotion,
6958
8144
  pageVisible: !this.#document.hidden,
6959
8145
  sections,
8146
+ targets: this.#targets?.snapshot(this.#trackTargets),
8147
+ samplePath: (path, progress) => this.#pathSampler.sample(path, progress),
6960
8148
  reaction
6961
8149
  };
6962
8150
  return {
@@ -6977,11 +8165,26 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
6977
8165
  overrides.observeEvent(reaction.name, reaction.source);
6978
8166
  }
6979
8167
  const request = overrides.compose(ordinary, world);
8168
+ this.#trackTargets = request.motion?.trackTarget === true;
6980
8169
  if (this.#reducedMotion) {
6981
8170
  delete request.motion;
6982
8171
  if (request.capability === "locomotion") delete request.capability;
6983
8172
  if (!request.state) request.state = "idle";
6984
8173
  }
8174
+ const interaction = this.#options.interaction || {};
8175
+ if (this.#dragging) {
8176
+ delete request.motion;
8177
+ delete request.capability;
8178
+ request.state = interaction.dragState ?? "scroll:fly";
8179
+ } else if (this.#throwing) {
8180
+ delete request.motion;
8181
+ delete request.capability;
8182
+ request.state = this.#throwVelocity.y < 0 ? interaction.riseState ?? "scroll:fly" : interaction.fallState ?? "scroll:fall";
8183
+ } else if (this.#landUntil > world.now) {
8184
+ delete request.motion;
8185
+ delete request.capability;
8186
+ request.state = interaction.landState ?? "success";
8187
+ }
6985
8188
  const diagnostics = [];
6986
8189
  if (reaction) {
6987
8190
  this.#seenReaction = reaction.id;
@@ -7040,19 +8243,74 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7040
8243
  }
7041
8244
  return this.#frameActive(frame);
7042
8245
  }
8246
+ #advanceThrow(snapshot) {
8247
+ if (!this.#throwing) return;
8248
+ const interaction = this.#options.interaction || {};
8249
+ const dt = snapshot.frame.stepMs / 1e3;
8250
+ const bounce = interaction.bounce ?? DEFAULT_BOUNCE;
8251
+ const halfWidth = snapshot.characterSize.width / 2;
8252
+ const halfHeight = snapshot.characterSize.height / 2;
8253
+ const floor = snapshot.world.viewport.height - halfHeight - (interaction.floorInset ?? DEFAULT_FLOOR_INSET);
8254
+ let velocityX = this.#throwVelocity.x;
8255
+ let velocityY = this.#throwVelocity.y + (interaction.gravity ?? DEFAULT_GRAVITY) * dt;
8256
+ let x = this.#position.x + velocityX * dt;
8257
+ let y = this.#position.y + velocityY * dt;
8258
+ const right = snapshot.world.viewport.width - halfWidth;
8259
+ if (x < halfWidth) {
8260
+ x = halfWidth;
8261
+ velocityX = Math.abs(velocityX) * bounce;
8262
+ } else if (x > right) {
8263
+ x = right;
8264
+ velocityX = -Math.abs(velocityX) * bounce;
8265
+ }
8266
+ if (y < halfHeight) {
8267
+ y = halfHeight;
8268
+ velocityY = Math.abs(velocityY) * bounce;
8269
+ }
8270
+ const next = { x, y };
8271
+ if (interaction.catchTarget && this.#targets?.contains(
8272
+ interaction.catchTarget,
8273
+ next,
8274
+ interaction.catchMargin ?? 24
8275
+ ) && this.#catchCharacter(interaction.catchTarget)) {
8276
+ return;
8277
+ }
8278
+ if (y >= floor) {
8279
+ this.#position = { x, y: floor };
8280
+ this.#landCharacter();
8281
+ return;
8282
+ }
8283
+ this.#position = next;
8284
+ this.#throwVelocity = { x: velocityX * 0.995, y: velocityY };
8285
+ }
7043
8286
  #calculatePresentation(snapshot, evaluation) {
7044
8287
  const { frame, characterSize, world } = snapshot;
7045
8288
  const { request } = evaluation;
7046
8289
  const loaded = frame.loaded.content;
7047
8290
  if (!this.#reducedMotion)
7048
8291
  this.#animationClock += Math.max(0, frame.elapsed);
8292
+ this.#advanceThrow(snapshot);
7049
8293
  const requested = request.capability === "locomotion" && request.motion ? loaded.directionalStates?.[directionTo(
7050
8294
  { x: 0, y: 0 },
7051
8295
  { x: request.motion.x, y: request.motion.y }
7052
8296
  )] : resolveCapabilityName(request.state, loaded);
7053
8297
  const resolved = resolveState(requested, loaded.states);
7054
8298
  let lift = 0;
7055
- if (request.motion) {
8299
+ if (request.motion?.direct) {
8300
+ const distance = request.motion.maxDistance ?? 0;
8301
+ this.#position = clampPosition(
8302
+ {
8303
+ x: this.#position.x + request.motion.x * distance,
8304
+ y: this.#position.y + request.motion.y * distance
8305
+ },
8306
+ world.viewport,
8307
+ characterSize.width,
8308
+ characterSize.height
8309
+ );
8310
+ lift = request.motion.lift ?? 0;
8311
+ this.#locomotionState = "";
8312
+ this.#locomotionElapsed = 0;
8313
+ } else if (request.motion) {
7056
8314
  if (this.#locomotionState !== resolved.name) {
7057
8315
  this.#locomotionState = resolved.name;
7058
8316
  this.#locomotionElapsed = 0;
@@ -7083,7 +8341,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7083
8341
  characterSize.width,
7084
8342
  characterSize.height
7085
8343
  );
7086
- lift = locomotionSample(loaded.locomotionMotion, nextCycles).lift * characterSize.height;
8344
+ lift = request.motion.lift ?? locomotionSample(loaded.locomotionMotion, nextCycles).lift * characterSize.height;
7087
8345
  } else {
7088
8346
  this.#locomotionState = "";
7089
8347
  this.#locomotionElapsed = 0;
@@ -7117,9 +8375,15 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7117
8375
  presentation.lift
7118
8376
  );
7119
8377
  if (!this.#frameActive(frame)) return;
8378
+ this.#interactionController?.render(
8379
+ this.#position,
8380
+ snapshot.characterSize,
8381
+ presentation.lift
8382
+ );
8383
+ if (!this.#frameActive(frame)) return;
7120
8384
  this.#lastTick = frame.now;
7121
8385
  this.#schedulePlan(frame);
7122
- if (this.#frameActive(frame) && (evaluation.request.motion || frame.input.reaction))
8386
+ if (this.#frameActive(frame) && (evaluation.request.motion || frame.input.reaction || this.#throwing || this.#dragging))
7123
8387
  this.#wake();
7124
8388
  }
7125
8389
  #endPage() {
@@ -7202,6 +8466,24 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7202
8466
  y: Math.max(height / 2, Math.min(viewport.height - height / 2, point.y))
7203
8467
  };
7204
8468
  }
8469
+ function characterTargetPoint(target, anchor, character) {
8470
+ const center = {
8471
+ x: target.left + target.width / 2,
8472
+ y: target.top + target.height / 2
8473
+ };
8474
+ switch (anchor) {
8475
+ case "top":
8476
+ return { x: center.x, y: target.top - character.height / 2 };
8477
+ case "right":
8478
+ return { x: target.right + character.width / 2, y: center.y };
8479
+ case "bottom":
8480
+ return { x: center.x, y: target.bottom + character.height / 2 };
8481
+ case "left":
8482
+ return { x: target.left - character.width / 2, y: center.y };
8483
+ default:
8484
+ return center;
8485
+ }
8486
+ }
7205
8487
 
7206
8488
  // packages/runtime/src/web-component.ts
7207
8489
  var PEEKLING_ELEMENT_TAG = "peekling-character";
@@ -7436,7 +8718,7 @@ ${styles.integrity ?? ""}` === this.#styleKey) return;
7436
8718
  }
7437
8719
 
7438
8720
  // packages/runtime/src/browser.ts
7439
- var browserCssIntegrity = "sha256-w8BENjaJ/B1wtpk3iuh7jT++FlbT5+fx5BBlvePx1qo=" ? "sha256-w8BENjaJ/B1wtpk3iuh7jT++FlbT5+fx5BBlvePx1qo=" : void 0;
8721
+ var browserCssIntegrity = "sha256-G1Pmoq2z4+o4mTD2uGdP0s6zJFJB7ZZbDgIGlBeFYsM=" ? "sha256-G1Pmoq2z4+o4mTD2uGdP0s6zJFJB7ZZbDgIGlBeFYsM=" : void 0;
7440
8722
  var browserScript = globalThis.document?.currentScript;
7441
8723
  if (browserScript?.src) {
7442
8724
  setBrowserStyleAsset({