PanSession.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { frame, isPrimaryPointer, cancelFrame, frameData } from 'motion-dom';
  2. import { secondsToMilliseconds, millisecondsToSeconds } from 'motion-utils';
  3. import { addPointerEvent } from '../../events/add-pointer-event.mjs';
  4. import { extractEventInfo } from '../../events/event-info.mjs';
  5. import { distance2D } from '../../utils/distance.mjs';
  6. import { pipe } from '../../utils/pipe.mjs';
  7. /**
  8. * @internal
  9. */
  10. class PanSession {
  11. constructor(event, handlers, { transformPagePoint, contextWindow, dragSnapToOrigin = false, } = {}) {
  12. /**
  13. * @internal
  14. */
  15. this.startEvent = null;
  16. /**
  17. * @internal
  18. */
  19. this.lastMoveEvent = null;
  20. /**
  21. * @internal
  22. */
  23. this.lastMoveEventInfo = null;
  24. /**
  25. * @internal
  26. */
  27. this.handlers = {};
  28. /**
  29. * @internal
  30. */
  31. this.contextWindow = window;
  32. this.updatePoint = () => {
  33. if (!(this.lastMoveEvent && this.lastMoveEventInfo))
  34. return;
  35. const info = getPanInfo(this.lastMoveEventInfo, this.history);
  36. const isPanStarted = this.startEvent !== null;
  37. // Only start panning if the offset is larger than 3 pixels. If we make it
  38. // any larger than this we'll want to reset the pointer history
  39. // on the first update to avoid visual snapping to the cursoe.
  40. const isDistancePastThreshold = distance2D(info.offset, { x: 0, y: 0 }) >= 3;
  41. if (!isPanStarted && !isDistancePastThreshold)
  42. return;
  43. const { point } = info;
  44. const { timestamp } = frameData;
  45. this.history.push({ ...point, timestamp });
  46. const { onStart, onMove } = this.handlers;
  47. if (!isPanStarted) {
  48. onStart && onStart(this.lastMoveEvent, info);
  49. this.startEvent = this.lastMoveEvent;
  50. }
  51. onMove && onMove(this.lastMoveEvent, info);
  52. };
  53. this.handlePointerMove = (event, info) => {
  54. this.lastMoveEvent = event;
  55. this.lastMoveEventInfo = transformPoint(info, this.transformPagePoint);
  56. // Throttle mouse move event to once per frame
  57. frame.update(this.updatePoint, true);
  58. };
  59. this.handlePointerUp = (event, info) => {
  60. this.end();
  61. const { onEnd, onSessionEnd, resumeAnimation } = this.handlers;
  62. if (this.dragSnapToOrigin)
  63. resumeAnimation && resumeAnimation();
  64. if (!(this.lastMoveEvent && this.lastMoveEventInfo))
  65. return;
  66. const panInfo = getPanInfo(event.type === "pointercancel"
  67. ? this.lastMoveEventInfo
  68. : transformPoint(info, this.transformPagePoint), this.history);
  69. if (this.startEvent && onEnd) {
  70. onEnd(event, panInfo);
  71. }
  72. onSessionEnd && onSessionEnd(event, panInfo);
  73. };
  74. // If we have more than one touch, don't start detecting this gesture
  75. if (!isPrimaryPointer(event))
  76. return;
  77. this.dragSnapToOrigin = dragSnapToOrigin;
  78. this.handlers = handlers;
  79. this.transformPagePoint = transformPagePoint;
  80. this.contextWindow = contextWindow || window;
  81. const info = extractEventInfo(event);
  82. const initialInfo = transformPoint(info, this.transformPagePoint);
  83. const { point } = initialInfo;
  84. const { timestamp } = frameData;
  85. this.history = [{ ...point, timestamp }];
  86. const { onSessionStart } = handlers;
  87. onSessionStart &&
  88. onSessionStart(event, getPanInfo(initialInfo, this.history));
  89. this.removeListeners = pipe(addPointerEvent(this.contextWindow, "pointermove", this.handlePointerMove), addPointerEvent(this.contextWindow, "pointerup", this.handlePointerUp), addPointerEvent(this.contextWindow, "pointercancel", this.handlePointerUp));
  90. }
  91. updateHandlers(handlers) {
  92. this.handlers = handlers;
  93. }
  94. end() {
  95. this.removeListeners && this.removeListeners();
  96. cancelFrame(this.updatePoint);
  97. }
  98. }
  99. function transformPoint(info, transformPagePoint) {
  100. return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
  101. }
  102. function subtractPoint(a, b) {
  103. return { x: a.x - b.x, y: a.y - b.y };
  104. }
  105. function getPanInfo({ point }, history) {
  106. return {
  107. point,
  108. delta: subtractPoint(point, lastDevicePoint(history)),
  109. offset: subtractPoint(point, startDevicePoint(history)),
  110. velocity: getVelocity(history, 0.1),
  111. };
  112. }
  113. function startDevicePoint(history) {
  114. return history[0];
  115. }
  116. function lastDevicePoint(history) {
  117. return history[history.length - 1];
  118. }
  119. function getVelocity(history, timeDelta) {
  120. if (history.length < 2) {
  121. return { x: 0, y: 0 };
  122. }
  123. let i = history.length - 1;
  124. let timestampedPoint = null;
  125. const lastPoint = lastDevicePoint(history);
  126. while (i >= 0) {
  127. timestampedPoint = history[i];
  128. if (lastPoint.timestamp - timestampedPoint.timestamp >
  129. secondsToMilliseconds(timeDelta)) {
  130. break;
  131. }
  132. i--;
  133. }
  134. if (!timestampedPoint) {
  135. return { x: 0, y: 0 };
  136. }
  137. const time = millisecondsToSeconds(lastPoint.timestamp - timestampedPoint.timestamp);
  138. if (time === 0) {
  139. return { x: 0, y: 0 };
  140. }
  141. const currentVelocity = {
  142. x: (lastPoint.x - timestampedPoint.x) / time,
  143. y: (lastPoint.y - timestampedPoint.y) / time,
  144. };
  145. if (currentVelocity.x === Infinity) {
  146. currentVelocity.x = 0;
  147. }
  148. if (currentVelocity.y === Infinity) {
  149. currentVelocity.y = 0;
  150. }
  151. return currentVelocity;
  152. }
  153. export { PanSession };