motion-v
Version:
<p align="center"> <img width="100" height="100" alt="Motion logo" src="https://user-images.githubusercontent.com/7850794/164965523-3eced4c4-6020-467e-acde-f11b7900ad62.png" /> </p> <h1 align="center">Motion for Vue</h1>
451 lines (450 loc) • 15.9 kB
JavaScript
import { addPointerEvent } from "../../../events/add-pointer-event.mjs";
import { extractEventInfo } from "../../../events/event-info.mjs";
import { addDomEvent } from "../../../events/add-dom-event.mjs";
import { getGlobalLock } from "./lock.mjs";
import { applyConstraints, calcRelativeConstraints, resolveDragElastic, calcViewportConstraints, rebaseAxisConstraints, calcOrigin, defaultElastic } from "./utils/constraints.mjs";
import { isHTMLElement } from "./utils/is.mjs";
import { PanSession } from "../pan/PanSession.mjs";
import { calcLength } from "../../../projection/geometry/delta-calc.mjs";
import { createBox } from "../../../projection/geometry/models.mjs";
import { eachAxis } from "../../../projection/utils/each-axis.mjs";
import { addValueToWillChange } from "../../../value/use-will-change/add-will-change.mjs";
import { measurePageBox } from "../../../projection/utils/measure.mjs";
import { convertBoxToBoundingBox, convertBoundingBoxToBox } from "../../../projection/conversion.mjs";
import { animateMotionValue } from "../../../external/.pnpm/framer-motion@12.23.12/external/framer-motion/dist/es/animation/interfaces/motion-value.mjs";
import { invariant } from "hey-listen";
import { isPresent } from "../../../state/utils/is-present.mjs";
import { getContextWindow } from "../../../utils/get-context-window.mjs";
import { percent } from "../../../external/.pnpm/motion-dom@12.23.12/external/motion-dom/dist/es/value/types/numbers/units.mjs";
import { frame } from "../../../external/.pnpm/motion-dom@12.23.12/external/motion-dom/dist/es/frameloop/frame.mjs";
import { mixNumber } from "../../../external/.pnpm/motion-dom@12.23.12/external/motion-dom/dist/es/utils/mix/number.mjs";
const elementDragControls = /* @__PURE__ */ new WeakMap();
class VisualElementDragControls {
constructor(visualElement) {
this.openGlobalLock = null;
this.isDragging = false;
this.currentDirection = null;
this.originPoint = { x: 0, y: 0 };
this.constraints = false;
this.hasMutatedConstraints = false;
this.elastic = createBox();
this.visualElement = visualElement;
}
start(originEvent, { snapToCursor = false } = {}) {
if (!isPresent(this.visualElement))
return;
const onSessionStart = (event) => {
const { dragSnapToOrigin: dragSnapToOrigin2 } = this.getProps();
dragSnapToOrigin2 ? this.pauseAnimation() : this.stopAnimation();
if (snapToCursor) {
this.snapToCursor(extractEventInfo(event, "page").point);
}
};
const onStart = (event, info) => {
const { drag, dragPropagation, onDragStart } = this.getProps();
if (drag && !dragPropagation) {
if (this.openGlobalLock)
this.openGlobalLock();
this.openGlobalLock = getGlobalLock(drag);
if (!this.openGlobalLock)
return;
}
this.isDragging = true;
this.currentDirection = null;
this.resolveConstraints();
if (this.visualElement.projection) {
this.visualElement.projection.isAnimationBlocked = true;
this.visualElement.projection.target = void 0;
}
eachAxis((axis) => {
let current = this.getAxisMotionValue(axis).get() || 0;
if (percent.test(current)) {
const { projection } = this.visualElement;
if (projection && projection.layout) {
const measuredAxis = projection.layout.layoutBox[axis];
if (measuredAxis) {
const length = calcLength(measuredAxis);
current = length * (parseFloat(current) / 100);
}
}
}
this.originPoint[axis] = current;
});
if (onDragStart) {
frame.postRender(() => onDragStart(event, info));
}
addValueToWillChange(this.visualElement, "transform");
const state = this.visualElement.state;
state.setActive("whileDrag", true);
};
const onMove = (event, info) => {
const {
dragPropagation,
dragDirectionLock,
onDirectionLock,
onDrag
} = this.getProps();
if (!dragPropagation && !this.openGlobalLock)
return;
const { offset } = info;
if (dragDirectionLock && this.currentDirection === null) {
this.currentDirection = getCurrentDirection(offset);
if (this.currentDirection !== null) {
onDirectionLock && onDirectionLock(this.currentDirection);
}
return;
}
this.updateAxis("x", info.point, offset);
this.updateAxis("y", info.point, offset);
this.visualElement.render();
onDrag && onDrag(event, info);
};
const onSessionEnd = (event, info) => this.stop(event, info);
const resumeAnimation = () => eachAxis(
(axis) => {
var _a;
return this.getAnimationState(axis) === "paused" && ((_a = this.getAxisMotionValue(axis).animation) == null ? void 0 : _a.play());
}
);
const { dragSnapToOrigin } = this.getProps();
this.panSession = new PanSession(
originEvent,
{
onSessionStart,
onStart,
onMove,
onSessionEnd,
resumeAnimation
},
{
transformPagePoint: this.visualElement.getTransformPagePoint(),
dragSnapToOrigin,
contextWindow: getContextWindow(this.visualElement)
}
);
}
stop(event, info) {
const isDragging = this.isDragging;
this.cancel();
if (!isDragging)
return;
const { velocity } = info;
this.startAnimation(velocity);
const { onDragEnd } = this.getProps();
if (onDragEnd) {
frame.postRender(() => onDragEnd(event, info));
}
}
cancel() {
this.isDragging = false;
const { projection, animationState } = this.visualElement;
if (projection) {
projection.isAnimationBlocked = false;
}
this.panSession && this.panSession.end();
this.panSession = void 0;
const { dragPropagation } = this.getProps();
if (!dragPropagation && this.openGlobalLock) {
this.openGlobalLock();
this.openGlobalLock = null;
}
const state = this.visualElement.state;
state.setActive("whileDrag", false);
}
updateAxis(axis, _point, offset) {
const { drag } = this.getProps();
if (!offset || !shouldDrag(axis, drag, this.currentDirection))
return;
const axisValue = this.getAxisMotionValue(axis);
let next = this.originPoint[axis] + offset[axis];
if (this.constraints && this.constraints[axis]) {
next = applyConstraints(
next,
this.constraints[axis],
this.elastic[axis]
);
}
axisValue.set(next);
}
resolveConstraints() {
var _a;
const { dragConstraints, dragElastic } = this.getProps();
const layout = this.visualElement.projection && !this.visualElement.projection.layout ? this.visualElement.projection.measure(false) : (_a = this.visualElement.projection) == null ? void 0 : _a.layout;
const prevConstraints = this.constraints;
if (dragConstraints && isHTMLElement(dragConstraints)) {
if (!this.constraints) {
this.constraints = this.resolveRefConstraints();
}
} else {
if (dragConstraints && layout) {
this.constraints = calcRelativeConstraints(
layout.layoutBox,
dragConstraints
);
} else {
this.constraints = false;
}
}
this.elastic = resolveDragElastic(dragElastic);
if (prevConstraints !== this.constraints && layout && this.constraints && !this.hasMutatedConstraints) {
eachAxis((axis) => {
if (this.constraints !== false && this.getAxisMotionValue(axis)) {
this.constraints[axis] = rebaseAxisConstraints(
layout.layoutBox[axis],
this.constraints[axis]
);
}
});
}
}
resolveRefConstraints() {
const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
if (!constraints || !isHTMLElement(constraints))
return false;
const constraintsElement = constraints;
invariant(
constraintsElement !== null,
"If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop."
);
const { projection } = this.visualElement;
if (!projection || !projection.layout)
return false;
const constraintsBox = measurePageBox(
constraintsElement,
projection.root,
this.visualElement.getTransformPagePoint()
);
let measuredConstraints = calcViewportConstraints(
projection.layout.layoutBox,
constraintsBox
);
if (onMeasureDragConstraints) {
const userConstraints = onMeasureDragConstraints(
convertBoxToBoundingBox(measuredConstraints)
);
this.hasMutatedConstraints = !!userConstraints;
if (userConstraints) {
measuredConstraints = convertBoundingBoxToBox(userConstraints);
}
}
return measuredConstraints;
}
startAnimation(velocity) {
const {
drag,
dragMomentum,
dragElastic,
dragTransition,
dragSnapToOrigin,
onDragTransitionEnd
} = this.getProps();
const constraints = this.constraints || {};
const momentumAnimations = eachAxis((axis) => {
if (!shouldDrag(axis, drag, this.currentDirection)) {
return;
}
let transition = constraints && constraints[axis] || {};
if (dragSnapToOrigin)
transition = { min: 0, max: 0 };
const bounceStiffness = dragElastic ? 200 : 1e6;
const bounceDamping = dragElastic ? 40 : 1e7;
const inertia = {
type: "inertia",
velocity: dragMomentum ? velocity[axis] : 0,
bounceStiffness,
bounceDamping,
timeConstant: 750,
restDelta: 1,
restSpeed: 10,
...dragTransition,
...transition
};
return this.startAxisValueAnimation(axis, inertia);
});
return Promise.all(momentumAnimations).then(onDragTransitionEnd);
}
startAxisValueAnimation(axis, transition) {
const axisValue = this.getAxisMotionValue(axis);
addValueToWillChange(this.visualElement, axis);
return axisValue.start(
animateMotionValue(
axis,
axisValue,
0,
transition,
this.visualElement,
false
)
);
}
stopAnimation() {
if (!isPresent(this.visualElement))
return;
eachAxis((axis) => this.getAxisMotionValue(axis).stop());
}
pauseAnimation() {
eachAxis((axis) => {
var _a;
return (_a = this.getAxisMotionValue(axis).animation) == null ? void 0 : _a.pause();
});
}
getAnimationState(axis) {
var _a;
return (_a = this.getAxisMotionValue(axis).animation) == null ? void 0 : _a.state;
}
/**
* Drag works differently depending on which props are provided.
*
* - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
* - Otherwise, we apply the delta to the x/y motion values.
*/
getAxisMotionValue(axis) {
const dragKey = `_drag${axis.toUpperCase()}`;
const props = this.visualElement.getProps();
const externalMotionValue = props[dragKey];
return externalMotionValue || this.visualElement.getValue(
axis,
(props.initial ? props.initial[axis] : void 0) || 0
);
}
snapToCursor(point) {
eachAxis((axis) => {
const { drag } = this.getProps();
if (!shouldDrag(axis, drag, this.currentDirection))
return;
const { projection } = this.visualElement;
const axisValue = this.getAxisMotionValue(axis);
if (projection && projection.layout) {
const { min, max } = projection.layout.layoutBox[axis];
axisValue.set(point[axis] - mixNumber(min, max, 0.5));
}
});
}
/**
* When the viewport resizes we want to check if the measured constraints
* have changed and, if so, reposition the element within those new constraints
* relative to where it was before the resize.
*/
scalePositionWithinConstraints() {
if (!this.visualElement.current)
return;
const { drag, dragConstraints } = this.getProps();
const { projection } = this.visualElement;
if (!isHTMLElement(dragConstraints) || !projection || !this.constraints)
return;
this.stopAnimation();
const boxProgress = { x: 0, y: 0 };
eachAxis((axis) => {
const axisValue = this.getAxisMotionValue(axis);
if (axisValue && this.constraints !== false) {
const latest = axisValue.get();
boxProgress[axis] = calcOrigin(
{ min: latest, max: latest },
this.constraints[axis]
);
}
});
const { transformTemplate } = this.visualElement.getProps();
this.visualElement.current.style.transform = transformTemplate ? transformTemplate({}, "") : "none";
projection.root && projection.root.updateScroll();
projection.updateLayout();
this.resolveConstraints();
eachAxis((axis) => {
if (!shouldDrag(axis, drag, null))
return;
const axisValue = this.getAxisMotionValue(axis);
const { min, max } = this.constraints[axis];
axisValue.set(mixNumber(min, max, boxProgress[axis]));
});
}
addListeners() {
if (!this.visualElement.current)
return;
elementDragControls.set(this.visualElement, this);
const element = this.visualElement.current;
const stopPointerListener = addPointerEvent(
element,
"pointerdown",
(event) => {
const { drag, dragListener = true } = this.getProps();
drag && dragListener && this.start(event);
}
);
const measureDragConstraints = () => {
const { dragConstraints } = this.getProps();
if (isHTMLElement(dragConstraints)) {
this.constraints = this.resolveRefConstraints();
}
};
const { projection } = this.visualElement;
const stopMeasureLayoutListener = projection.addEventListener(
"measure",
measureDragConstraints
);
if (projection && !projection.layout) {
projection.root && projection.root.updateScroll();
projection.updateLayout();
}
frame.read(measureDragConstraints);
const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
const stopLayoutUpdateListener = projection.addEventListener(
"didUpdate",
({ delta, hasLayoutChanged }) => {
if (this.isDragging && hasLayoutChanged) {
eachAxis((axis) => {
const motionValue = this.getAxisMotionValue(axis);
if (!motionValue)
return;
this.originPoint[axis] += delta[axis].translate;
motionValue.set(
motionValue.get() + delta[axis].translate
);
});
this.visualElement.render();
}
}
);
return () => {
stopResizeListener();
stopPointerListener();
stopMeasureLayoutListener();
stopLayoutUpdateListener && stopLayoutUpdateListener();
};
}
getProps() {
const props = this.visualElement.getProps();
const {
drag = false,
dragDirectionLock = false,
dragPropagation = false,
dragConstraints = false,
dragElastic = defaultElastic,
dragMomentum = true
} = props;
return {
...props,
drag,
dragDirectionLock,
dragPropagation,
dragConstraints,
dragElastic,
dragMomentum
};
}
}
function shouldDrag(direction, drag, currentDirection) {
return (drag === true || drag === direction) && (currentDirection === null || currentDirection === direction);
}
function getCurrentDirection(offset, lockThreshold = 10) {
let direction = null;
if (Math.abs(offset.y) > lockThreshold) {
direction = "y";
} else if (Math.abs(offset.x) > lockThreshold) {
direction = "x";
}
return direction;
}
export {
VisualElementDragControls,
elementDragControls
};