UNPKG

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
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 };