motion-v
Version:
<h1 align="center"> <img width="35" height="35" alt="Motion logo" src="https://github.com/user-attachments/assets/00d6d1c3-72c4-4c2f-a664-69da13182ffc" /><br />Motion for Vue</h1>
307 lines (306 loc) • 12.5 kB
JavaScript
import { getContextWindow } from "../../../utils/get-context-window.mjs";
import { extractEventInfo } from "../../../events/event-info.mjs";
import { addDomEvent } from "../../../events/add-dom-event.mjs";
import { addPointerEvent } from "../../../events/add-pointer-event.mjs";
import { isHTMLElement } from "./utils/is.mjs";
import { PanSession } from "../pan/PanSession.mjs";
import { getGlobalLock } from "./lock.mjs";
import { applyConstraints, calcOrigin, calcRelativeConstraints, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, resolveDragElastic } from "./utils/constraints.mjs";
import { addValueToWillChange, animateMotionValue, calcLength, convertBoundingBoxToBox, convertBoxToBoundingBox, createBox, eachAxis, frame, measurePageBox, mixNumber, percent } from "motion-dom";
import { invariant } from "hey-listen";
const elementDragControls = /* @__PURE__ */ new WeakMap();
var VisualElementDragControls = class {
constructor(state) {
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.state = state;
}
get visualElement() {
return this.state.visualElement;
}
start(originEvent, { snapToCursor = false } = {}) {
const onSessionStart = (event) => {
if (snapToCursor) this.stopAnimation();
else this.pauseAnimation();
if (snapToCursor) this.snapToCursor(extractEventInfo(event, "page").point);
};
const onStart = (event, info) => {
this.stopAnimation();
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) current = calcLength(measuredAxis) * (parseFloat(current) / 100);
}
}
this.originPoint[axis] = current;
});
if (onDragStart) frame.postRender(() => onDragStart(event, info));
addValueToWillChange(this.visualElement, "transform");
this.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) => this.getAnimationState(axis) === "paused" && this.getAxisMotionValue(axis).animation?.play());
const { dragSnapToOrigin } = this.getProps();
this.panSession = new PanSession(originEvent, {
onSessionStart,
onStart,
onMove,
onSessionEnd,
resumeAnimation
}, {
transformPagePoint: this.visualElement.getTransformPagePoint(),
dragSnapToOrigin,
contextWindow: getContextWindow(this.visualElement),
element: this.state.element
});
}
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 } = 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;
}
this.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() {
const { dragConstraints, dragElastic } = this.getProps();
const layout = this.visualElement.projection && !this.visualElement.projection.layout ? this.visualElement.projection.measure(false) : this.visualElement.projection?.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 (!this.visualElement.projection?.isPresent) return;
eachAxis((axis) => this.getAxisMotionValue(axis).stop());
}
pauseAnimation() {
eachAxis((axis) => this.getAxisMotionValue(axis).animation?.pause());
}
getAnimationState(axis) {
return this.getAxisMotionValue(axis).animation?.state;
}
getAxisMotionValue(axis) {
const dragKey = `_drag${axis.toUpperCase()}`;
const props = this.visualElement.getProps();
return props[dragKey] || 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, .5));
}
});
}
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.state.element.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.state.element) return;
elementDragControls.set(this.visualElement, this);
const element = this.state.element;
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$1 = this.getAxisMotionValue(axis);
if (!motionValue$1) return;
this.originPoint[axis] += delta[axis].translate;
motionValue$1.set(motionValue$1.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 };