UNPKG

framer-motion

Version:

A simple and powerful React animation library

447 lines (444 loc) • 22.1 kB
import { __assign } from 'tslib'; import { invariant } from 'hey-listen'; import { PanSession } from '../PanSession.mjs'; import { getGlobalLock } from './utils/lock.mjs'; import { isRefObject } from '../../utils/is-ref-object.mjs'; import { addPointerEvent } from '../../events/use-pointer-event.mjs'; import { applyConstraints, calcRelativeConstraints, resolveDragElastic, rebaseAxisConstraints, calcViewportConstraints, calcOrigin, defaultElastic } from './utils/constraints.mjs'; import { AnimationType } from '../../render/utils/types.mjs'; import { createBox } from '../../projection/geometry/models.mjs'; import { eachAxis } from '../../projection/utils/each-axis.mjs'; import { measurePageBox } from '../../projection/utils/measure.mjs'; import { extractEventInfo } from '../../events/event-info.mjs'; import { startAnimation } from '../../animation/utils/transitions.mjs'; import { convertBoxToBoundingBox, convertBoundingBoxToBox } from '../../projection/geometry/conversion.mjs'; import { addDomEvent } from '../../events/use-dom-event.mjs'; import { mix } from 'popmotion'; import { percent } from 'style-value-types'; import { calcLength } from '../../projection/geometry/delta-calc.mjs'; var elementDragControls = new WeakMap(); /** * */ // let latestPointerEvent: AnyPointerEvent var VisualElementDragControls = /** @class */ (function () { function VisualElementDragControls(visualElement) { // This is a reference to the global drag gesture lock, ensuring only one component // can "capture" the drag of one or both axes. // TODO: Look into moving this into pansession? this.openGlobalLock = null; this.isDragging = false; this.currentDirection = null; this.originPoint = { x: 0, y: 0 }; /** * The permitted boundaries of travel, in pixels. */ this.constraints = false; this.hasMutatedConstraints = false; /** * The per-axis resolved elastic values. */ this.elastic = createBox(); this.visualElement = visualElement; } VisualElementDragControls.prototype.start = function (originEvent, _a) { var _this = this; var _b = _a === void 0 ? {} : _a, _c = _b.snapToCursor, snapToCursor = _c === void 0 ? false : _c; /** * Don't start dragging if this component is exiting */ if (this.visualElement.isPresent === false) return; var onSessionStart = function (event) { // Stop any animations on both axis values immediately. This allows the user to throw and catch // the component. _this.stopAnimation(); if (snapToCursor) { _this.snapToCursor(extractEventInfo(event, "page").point); } }; var onStart = function (event, info) { var _a; // Attempt to grab the global drag gesture lock - maybe make this part of PanSession var _b = _this.getProps(), drag = _b.drag, dragPropagation = _b.dragPropagation, onDragStart = _b.onDragStart; if (drag && !dragPropagation) { if (_this.openGlobalLock) _this.openGlobalLock(); _this.openGlobalLock = getGlobalLock(drag); // If we don 't have the lock, don't start dragging 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 = undefined; } /** * Record gesture origin */ eachAxis(function (axis) { var _a, _b; var current = _this.getAxisMotionValue(axis).get() || 0; /** * If the MotionValue is a percentage value convert to px */ if (percent.test(current)) { var measuredAxis = (_b = (_a = _this.visualElement.projection) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.actual[axis]; if (measuredAxis) { var length_1 = calcLength(measuredAxis); current = length_1 * (parseFloat(current) / 100); } } _this.originPoint[axis] = current; }); // Fire onDragStart event onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(event, info); (_a = _this.visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(AnimationType.Drag, true); }; var onMove = function (event, info) { // latestPointerEvent = event var _a = _this.getProps(), dragPropagation = _a.dragPropagation, dragDirectionLock = _a.dragDirectionLock, onDirectionLock = _a.onDirectionLock, onDrag = _a.onDrag; // If we didn't successfully receive the gesture lock, early return. if (!dragPropagation && !_this.openGlobalLock) return; var offset = info.offset; // Attempt to detect drag direction if directionLock is true if (dragDirectionLock && _this.currentDirection === null) { _this.currentDirection = getCurrentDirection(offset); // If we've successfully set a direction, notify listener if (_this.currentDirection !== null) { onDirectionLock === null || onDirectionLock === void 0 ? void 0 : onDirectionLock(_this.currentDirection); } return; } // Update each point with the latest position _this.updateAxis("x", info.point, offset); _this.updateAxis("y", info.point, offset); /** * Ideally we would leave the renderer to fire naturally at the end of * this frame but if the element is about to change layout as the result * of a re-render we want to ensure the browser can read the latest * bounding box to ensure the pointer and element don't fall out of sync. */ _this.visualElement.syncRender(); /** * This must fire after the syncRender call as it might trigger a state * change which itself might trigger a layout update. */ onDrag === null || onDrag === void 0 ? void 0 : onDrag(event, info); }; var onSessionEnd = function (event, info) { return _this.stop(event, info); }; this.panSession = new PanSession(originEvent, { onSessionStart: onSessionStart, onStart: onStart, onMove: onMove, onSessionEnd: onSessionEnd, }, { transformPagePoint: this.visualElement.getTransformPagePoint() }); }; VisualElementDragControls.prototype.stop = function (event, info) { var isDragging = this.isDragging; this.cancel(); if (!isDragging) return; var velocity = info.velocity; this.startAnimation(velocity); var onDragEnd = this.getProps().onDragEnd; onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(event, info); }; VisualElementDragControls.prototype.cancel = function () { var _a, _b; this.isDragging = false; if (this.visualElement.projection) { this.visualElement.projection.isAnimationBlocked = false; } (_a = this.panSession) === null || _a === void 0 ? void 0 : _a.end(); this.panSession = undefined; var dragPropagation = this.getProps().dragPropagation; if (!dragPropagation && this.openGlobalLock) { this.openGlobalLock(); this.openGlobalLock = null; } (_b = this.visualElement.animationState) === null || _b === void 0 ? void 0 : _b.setActive(AnimationType.Drag, false); }; VisualElementDragControls.prototype.updateAxis = function (axis, _point, offset) { var drag = this.getProps().drag; // If we're not dragging this axis, do an early return. if (!offset || !shouldDrag(axis, drag, this.currentDirection)) return; var axisValue = this.getAxisMotionValue(axis); var next = this.originPoint[axis] + offset[axis]; // Apply constraints if (this.constraints && this.constraints[axis]) { next = applyConstraints(next, this.constraints[axis], this.elastic[axis]); } axisValue.set(next); }; VisualElementDragControls.prototype.resolveConstraints = function () { var _this = this; var _a = this.getProps(), dragConstraints = _a.dragConstraints, dragElastic = _a.dragElastic; var layout = (this.visualElement.projection || {}).layout; var prevConstraints = this.constraints; if (dragConstraints && isRefObject(dragConstraints)) { if (!this.constraints) { this.constraints = this.resolveRefConstraints(); } } else { if (dragConstraints && layout) { this.constraints = calcRelativeConstraints(layout.actual, dragConstraints); } else { this.constraints = false; } } this.elastic = resolveDragElastic(dragElastic); /** * If we're outputting to external MotionValues, we want to rebase the measured constraints * from viewport-relative to component-relative. */ if (prevConstraints !== this.constraints && layout && this.constraints && !this.hasMutatedConstraints) { eachAxis(function (axis) { if (_this.getAxisMotionValue(axis)) { _this.constraints[axis] = rebaseAxisConstraints(layout.actual[axis], _this.constraints[axis]); } }); } }; VisualElementDragControls.prototype.resolveRefConstraints = function () { var _a = this.getProps(), constraints = _a.dragConstraints, onMeasureDragConstraints = _a.onMeasureDragConstraints; if (!constraints || !isRefObject(constraints)) return false; var constraintsElement = constraints.current; invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop."); var projection = this.visualElement.projection; // TODO if (!projection || !projection.layout) return false; var constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint()); var measuredConstraints = calcViewportConstraints(projection.layout.actual, constraintsBox); /** * If there's an onMeasureDragConstraints listener we call it and * if different constraints are returned, set constraints to that */ if (onMeasureDragConstraints) { var userConstraints = onMeasureDragConstraints(convertBoxToBoundingBox(measuredConstraints)); this.hasMutatedConstraints = !!userConstraints; if (userConstraints) { measuredConstraints = convertBoundingBoxToBox(userConstraints); } } return measuredConstraints; }; VisualElementDragControls.prototype.startAnimation = function (velocity) { var _this = this; var _a = this.getProps(), drag = _a.drag, dragMomentum = _a.dragMomentum, dragElastic = _a.dragElastic, dragTransition = _a.dragTransition, dragSnapToOrigin = _a.dragSnapToOrigin, onDragTransitionEnd = _a.onDragTransitionEnd; var constraints = this.constraints || {}; var momentumAnimations = eachAxis(function (axis) { var _a; if (!shouldDrag(axis, drag, _this.currentDirection)) { return; } var transition = (_a = constraints === null || constraints === void 0 ? void 0 : constraints[axis]) !== null && _a !== void 0 ? _a : {}; if (dragSnapToOrigin) transition = { min: 0, max: 0 }; /** * Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame * of spring animations so we should look into adding a disable spring option to `inertia`. * We could do something here where we affect the `bounceStiffness` and `bounceDamping` * using the value of `dragElastic`. */ var bounceStiffness = dragElastic ? 200 : 1000000; var bounceDamping = dragElastic ? 40 : 10000000; var inertia = __assign(__assign({ type: "inertia", velocity: dragMomentum ? velocity[axis] : 0, bounceStiffness: bounceStiffness, bounceDamping: bounceDamping, timeConstant: 750, restDelta: 1, restSpeed: 10 }, dragTransition), transition); // If we're not animating on an externally-provided `MotionValue` we can use the // component's animation controls which will handle interactions with whileHover (etc), // otherwise we just have to animate the `MotionValue` itself. return _this.startAxisValueAnimation(axis, inertia); }); // Run all animations and then resolve the new drag constraints. return Promise.all(momentumAnimations).then(onDragTransitionEnd); }; VisualElementDragControls.prototype.startAxisValueAnimation = function (axis, transition) { var axisValue = this.getAxisMotionValue(axis); return startAnimation(axis, axisValue, 0, transition); }; VisualElementDragControls.prototype.stopAnimation = function () { var _this = this; eachAxis(function (axis) { return _this.getAxisMotionValue(axis).stop(); }); }; /** * 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. */ VisualElementDragControls.prototype.getAxisMotionValue = function (axis) { var _a, _b; var dragKey = "_drag" + axis.toUpperCase(); var externalMotionValue = this.visualElement.getProps()[dragKey]; return externalMotionValue ? externalMotionValue : this.visualElement.getValue(axis, (_b = (_a = this.visualElement.getProps().initial) === null || _a === void 0 ? void 0 : _a[axis]) !== null && _b !== void 0 ? _b : 0); }; VisualElementDragControls.prototype.snapToCursor = function (point) { var _this = this; eachAxis(function (axis) { var drag = _this.getProps().drag; // If we're not dragging this axis, do an early return. if (!shouldDrag(axis, drag, _this.currentDirection)) return; var projection = _this.visualElement.projection; var axisValue = _this.getAxisMotionValue(axis); if (projection && projection.layout) { var _a = projection.layout.actual[axis], min = _a.min, max = _a.max; axisValue.set(point[axis] - mix(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. */ VisualElementDragControls.prototype.scalePositionWithinConstraints = function () { var _this = this; var _a; var _b = this.getProps(), drag = _b.drag, dragConstraints = _b.dragConstraints; var projection = this.visualElement.projection; if (!isRefObject(dragConstraints) || !projection || !this.constraints) return; /** * Stop current animations as there can be visual glitching if we try to do * this mid-animation */ this.stopAnimation(); /** * Record the relative position of the dragged element relative to the * constraints box and save as a progress value. */ var boxProgress = { x: 0, y: 0 }; eachAxis(function (axis) { var axisValue = _this.getAxisMotionValue(axis); if (axisValue) { var latest = axisValue.get(); boxProgress[axis] = calcOrigin({ min: latest, max: latest }, _this.constraints[axis]); } }); /** * Update the layout of this element and resolve the latest drag constraints */ var transformTemplate = this.visualElement.getProps().transformTemplate; this.visualElement.getInstance().style.transform = transformTemplate ? transformTemplate({}, "") : "none"; (_a = projection.root) === null || _a === void 0 ? void 0 : _a.updateScroll(); projection.updateLayout(); this.resolveConstraints(); /** * For each axis, calculate the current progress of the layout axis * within the new constraints. */ eachAxis(function (axis) { if (!shouldDrag(axis, drag, null)) return; /** * Calculate a new transform based on the previous box progress */ var axisValue = _this.getAxisMotionValue(axis); var _a = _this.constraints[axis], min = _a.min, max = _a.max; axisValue.set(mix(min, max, boxProgress[axis])); }); }; VisualElementDragControls.prototype.addListeners = function () { var _this = this; var _a; elementDragControls.set(this.visualElement, this); var element = this.visualElement.getInstance(); /** * Attach a pointerdown event listener on this DOM element to initiate drag tracking. */ var stopPointerListener = addPointerEvent(element, "pointerdown", function (event) { var _a = _this.getProps(), drag = _a.drag, _b = _a.dragListener, dragListener = _b === void 0 ? true : _b; drag && dragListener && _this.start(event); }); var measureDragConstraints = function () { var dragConstraints = _this.getProps().dragConstraints; if (isRefObject(dragConstraints)) { _this.constraints = _this.resolveRefConstraints(); } }; var projection = this.visualElement.projection; var stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints); if (projection && !projection.layout) { (_a = projection.root) === null || _a === void 0 ? void 0 : _a.updateScroll(); projection.updateLayout(); } measureDragConstraints(); /** * Attach a window resize listener to scale the draggable target within its defined * constraints as the window resizes. */ var stopResizeListener = addDomEvent(window, "resize", function () { _this.scalePositionWithinConstraints(); }); /** * If the element's layout changes, calculate the delta and apply that to * the drag gesture's origin point. */ projection.addEventListener("didUpdate", (function (_a) { var delta = _a.delta, hasLayoutChanged = _a.hasLayoutChanged; if (_this.isDragging && hasLayoutChanged) { eachAxis(function (axis) { var motionValue = _this.getAxisMotionValue(axis); if (!motionValue) return; _this.originPoint[axis] += delta[axis].translate; motionValue.set(motionValue.get() + delta[axis].translate); }); _this.visualElement.syncRender(); } })); return function () { stopResizeListener(); stopPointerListener(); stopMeasureLayoutListener(); }; }; VisualElementDragControls.prototype.getProps = function () { var props = this.visualElement.getProps(); var _a = props.drag, drag = _a === void 0 ? false : _a, _b = props.dragDirectionLock, dragDirectionLock = _b === void 0 ? false : _b, _c = props.dragPropagation, dragPropagation = _c === void 0 ? false : _c, _d = props.dragConstraints, dragConstraints = _d === void 0 ? false : _d, _e = props.dragElastic, dragElastic = _e === void 0 ? defaultElastic : _e, _f = props.dragMomentum, dragMomentum = _f === void 0 ? true : _f; return __assign(__assign({}, props), { drag: drag, dragDirectionLock: dragDirectionLock, dragPropagation: dragPropagation, dragConstraints: dragConstraints, dragElastic: dragElastic, dragMomentum: dragMomentum }); }; return VisualElementDragControls; }()); function shouldDrag(direction, drag, currentDirection) { return ((drag === true || drag === direction) && (currentDirection === null || currentDirection === direction)); } /** * Based on an x/y offset determine the current drag direction. If both axis' offsets are lower * than the provided threshold, return `null`. * * @param offset - The x/y offset from origin. * @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction. */ function getCurrentDirection(offset, lockThreshold) { if (lockThreshold === void 0) { lockThreshold = 10; } var direction = null; if (Math.abs(offset.y) > lockThreshold) { direction = "y"; } else if (Math.abs(offset.x) > lockThreshold) { direction = "x"; } return direction; } export { VisualElementDragControls, elementDragControls };