UNPKG

svelte-motion

Version:

Svelte animation library based on the React library framer-motion.

595 lines (592 loc) 29.6 kB
/** based on framer-motion@4.1.15, Copyright (c) 2018 Framer B.V. */ import {fixed} from '../../utils/fix-process-env'; import { __rest, __spreadArray, __read } from 'tslib'; import { invariant } from 'hey-listen'; import { PanSession } from '../PanSession.js'; import { getGlobalLock } from './utils/lock.js'; import { isRefObject } from '../../utils/is-ref-object.js'; import { addPointerEvent } from '../../events/use-pointer-event.js'; import { addDomEvent } from '../../events/use-dom-event.js'; import { getViewportPointFromEvent } from '../../events/event-info.js'; import { axisBox, convertAxisBoxToBoundingBox, convertBoundingBoxToAxisBox } from '../../utils/geometry/index.js'; import { eachAxis } from '../../utils/each-axis.js'; import { calcRelativeConstraints, resolveDragElastic, rebaseAxisConstraints, calcViewportConstraints, applyConstraints, calcConstrainedMinPoint, calcPositionFromProgress, defaultElastic } from './utils/constraints.js'; import { getBoundingBox } from '../../render/dom/projection/measure.js'; import { calcOrigin } from '../../utils/geometry/delta-calc.js'; import { startAnimation } from '../../animation/utils/transitions.js'; import { AnimationType } from '../../render/utils/types.js'; import { collectProjectingAncestors, updateLayoutMeasurement, collectProjectingChildren } from '../../render/dom/projection/utils.js'; import { progress } from 'popmotion'; import { convertToRelativeProjection } from '../../render/dom/projection/convert-to-relative.js'; import { calcRelativeOffset } from '../../motion/features/layout/utils.js'; import { flushLayout, batchLayout } from '../../render/dom/utils/batch-layout.js'; import { flushSync } from 'framesync'; var elementDragControls = new WeakMap(); /** * */ var lastPointerEvent; var VisualElementDragControls = /** @class */ (function () { function VisualElementDragControls(_a) { var visualElement = _a.visualElement; /** * Track whether we're currently dragging. * * @internal */ this.isDragging = false; /** * The current direction of drag, or `null` if both. * * @internal */ this.currentDirection = null; /** * The permitted boundaries of travel, in pixels. * * @internal */ this.constraints = false; /** * The per-axis resolved elastic values. * * @internal */ this.elastic = axisBox(); /** * A reference to the host component's latest props. * * @internal */ this.props = {}; /** * @internal */ this.hasMutatedConstraints = false; /** * Track the initial position of the cursor relative to the dragging element * when dragging starts as a value of 0-1 on each axis. We then use this to calculate * an ideal bounding box for the VisualElement renderer to project into every frame. * * @internal */ this.cursorProgress = { x: 0.5, y: 0.5, }; // When updating _dragX, or _dragY instead of the VisualElement, // persist their values between drag gestures. this.originPoint = {}; // 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; /** * @internal */ this.panSession = null; this.visualElement = visualElement; this.visualElement.enableLayoutProjection(); elementDragControls.set(visualElement, this); } /** * Instantiate a PanSession for the drag gesture * * @public */ VisualElementDragControls.prototype.start = function (originEvent, _a) { var _this = this; var _b = _a === void 0 ? {} : _a, _c = _b.snapToCursor, snapToCursor = _c === void 0 ? false : _c, cursorProgress = _b.cursorProgress; var onSessionStart = function (event) { var _a; // Stop any animations on both axis values immediately. This allows the user to throw and catch // the component. _this.stopMotion(); /** * Save the initial point. We'll use this to calculate the pointer's position rather * than the one we receive when the gesture actually starts. By then, the pointer will * have already moved, and the perception will be of the pointer "slipping" across the element */ var initialPoint = getViewportPointFromEvent(event).point; (_a = _this.cancelLayout) === null || _a === void 0 ? void 0 : _a.call(_this); _this.cancelLayout = batchLayout(function (read, write) { var ancestors = collectProjectingAncestors(_this.visualElement); var children = collectProjectingChildren(_this.visualElement); var tree = __spreadArray(__spreadArray([], __read(ancestors)), __read(children)); var hasManuallySetCursorOrigin = false; /** * Apply a simple lock to the projection target. This ensures no animations * can run on the projection box while this lock is active. */ _this.isLayoutDrag() && _this.visualElement.lockProjectionTarget(); write(function () { tree.forEach(function (element) { return element.resetTransform(); }); }); read(function () { updateLayoutMeasurement(_this.visualElement); children.forEach(updateLayoutMeasurement); }); write(function () { tree.forEach(function (element) { return element.restoreTransform(); }); if (snapToCursor) { hasManuallySetCursorOrigin = _this.snapToCursor(initialPoint); } }); read(function () { var isRelativeDrag = Boolean(_this.getAxisMotionValue("x") && !_this.isExternalDrag()); if (!isRelativeDrag) { _this.visualElement.rebaseProjectionTarget(true, _this.visualElement.measureViewportBox(false)); } _this.visualElement.scheduleUpdateLayoutProjection(); /** * When dragging starts, we want to find where the cursor is relative to the bounding box * of the element. Every frame, we calculate a new bounding box using this relative position * and let the visualElement renderer figure out how to reproject the element into this bounding * box. * * By doing it this way, rather than applying an x/y transform directly to the element, * we can ensure the component always visually sticks to the cursor as we'd expect, even * if the DOM element itself changes layout as a result of React updates the user might * make based on the drag position. */ var projection = _this.visualElement.projection; eachAxis(function (axis) { if (!hasManuallySetCursorOrigin) { var _a = projection.target[axis], min = _a.min, max = _a.max; _this.cursorProgress[axis] = cursorProgress ? cursorProgress[axis] : progress(min, max, initialPoint[axis]); } /** * If we have external drag MotionValues, record their origin point. On pointermove * we'll apply the pan gesture offset directly to this value. */ var axisValue = _this.getAxisMotionValue(axis); if (axisValue) { _this.originPoint[axis] = axisValue.get(); } }); }); write(function () { flushSync.update(); flushSync.preRender(); flushSync.render(); flushSync.postRender(); }); read(function () { return _this.resolveDragConstraints(); }); }); }; var onStart = function (event, info) { var _a, _b, _c; // Attempt to grab the global drag gesture lock - maybe make this part of PanSession var _d = _this.props, drag = _d.drag, dragPropagation = _d.dragPropagation; 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; } flushLayout(); // Set current drag status _this.isDragging = true; _this.currentDirection = null; // Fire onDragStart event (_b = (_a = _this.props).onDragStart) === null || _b === void 0 ? void 0 : _b.call(_a, event, info); (_c = _this.visualElement.animationState) === null || _c === void 0 ? void 0 : _c.setActive(AnimationType.Drag, true); }; var onMove = function (event, info) { var _a, _b, _c, _d; var _e = _this.props, dragPropagation = _e.dragPropagation, dragDirectionLock = _e.dragDirectionLock; // 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) { (_b = (_a = _this.props).onDirectionLock) === null || _b === void 0 ? void 0 : _b.call(_a, _this.currentDirection); } return; } // Update each point with the latest position _this.updateAxis("x", info.point, offset); _this.updateAxis("y", info.point, offset); // Fire onDrag event (_d = (_c = _this.props).onDrag) === null || _d === void 0 ? void 0 : _d.call(_c, event, info); // Update the last pointer event lastPointerEvent = event; }; var onSessionEnd = function (event, info) { return _this.stop(event, info); }; var transformPagePoint = this.props.transformPagePoint; this.panSession = new PanSession(originEvent, { onSessionStart: onSessionStart, onStart: onStart, onMove: onMove, onSessionEnd: onSessionEnd, }, { transformPagePoint: transformPagePoint }); }; VisualElementDragControls.prototype.resolveDragConstraints = function () { var _this = this; var _a = this.props, dragConstraints = _a.dragConstraints, dragElastic = _a.dragElastic; var layout = this.visualElement.getLayoutState().layoutCorrected; if (dragConstraints) { this.constraints = isRefObject(dragConstraints) ? this.resolveRefConstraints(layout, dragConstraints) : calcRelativeConstraints(layout, 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 (this.constraints && !this.hasMutatedConstraints) { eachAxis(function (axis) { if (_this.getAxisMotionValue(axis)) { _this.constraints[axis] = rebaseAxisConstraints(layout[axis], _this.constraints[axis]); } }); } }; VisualElementDragControls.prototype.resolveRefConstraints = function (layoutBox, constraints) { var _a = this.props, onMeasureDragConstraints = _a.onMeasureDragConstraints, transformPagePoint = _a.transformPagePoint; 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."); this.constraintsBox = getBoundingBox(constraintsElement, transformPagePoint); var measuredConstraints = calcViewportConstraints(layoutBox, this.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(convertAxisBoxToBoundingBox(measuredConstraints)); this.hasMutatedConstraints = !!userConstraints; if (userConstraints) { measuredConstraints = convertBoundingBoxToAxisBox(userConstraints); } } return measuredConstraints; }; VisualElementDragControls.prototype.cancelDrag = function () { var _a, _b; this.visualElement.unlockProjectionTarget(); (_a = this.cancelLayout) === null || _a === void 0 ? void 0 : _a.call(this); this.isDragging = false; this.panSession && this.panSession.end(); this.panSession = null; if (!this.props.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.stop = function (event, info) { var _a, _b, _c; (_a = this.panSession) === null || _a === void 0 ? void 0 : _a.end(); this.panSession = null; var isDragging = this.isDragging; this.cancelDrag(); if (!isDragging) return; var velocity = info.velocity; this.animateDragEnd(velocity); (_c = (_b = this.props).onDragEnd) === null || _c === void 0 ? void 0 : _c.call(_b, event, info); }; VisualElementDragControls.prototype.snapToCursor = function (point) { var _this = this; return eachAxis(function (axis) { var drag = _this.props.drag; // If we're not dragging this axis, do an early return. if (!shouldDrag(axis, drag, _this.currentDirection)) return; var axisValue = _this.getAxisMotionValue(axis); if (axisValue) { var box = _this.visualElement.getLayoutState().layout; var length_1 = box[axis].max - box[axis].min; var center = box[axis].min + length_1 / 2; var offset = point[axis] - center; _this.originPoint[axis] = point[axis]; axisValue.set(offset); } else { _this.cursorProgress[axis] = 0.5; return true; } }).includes(true); }; /** * Update the specified axis with the latest pointer information. */ VisualElementDragControls.prototype.updateAxis = function (axis, point, offset) { var drag = this.props.drag; // If we're not dragging this axis, do an early return. if (!shouldDrag(axis, drag, this.currentDirection)) return; return this.getAxisMotionValue(axis) ? this.updateAxisMotionValue(axis, offset) : this.updateVisualElementAxis(axis, point); }; VisualElementDragControls.prototype.updateAxisMotionValue = function (axis, offset) { var axisValue = this.getAxisMotionValue(axis); if (!offset || !axisValue) return; var nextValue = this.originPoint[axis] + offset[axis]; var update = this.constraints ? applyConstraints(nextValue, this.constraints[axis], this.elastic[axis]) : nextValue; axisValue.set(update); }; VisualElementDragControls.prototype.updateVisualElementAxis = function (axis, point) { var _a; // Get the actual layout bounding box of the element var axisLayout = this.visualElement.getLayoutState().layout[axis]; // Calculate its current length. In the future we might want to lerp this to animate // between lengths if the layout changes as we change the DOM var axisLength = axisLayout.max - axisLayout.min; // Get the initial progress that the pointer sat on this axis on gesture start. var axisProgress = this.cursorProgress[axis]; // Calculate a new min point based on the latest pointer position, constraints and elastic var min = calcConstrainedMinPoint(point[axis], axisLength, axisProgress, (_a = this.constraints) === null || _a === void 0 ? void 0 : _a[axis], this.elastic[axis]); // Update the axis viewport target with this new min and the length this.visualElement.setProjectionTargetAxis(axis, min, min + axisLength); }; VisualElementDragControls.prototype.setProps = function (_a) { var _b = _a.drag, drag = _b === void 0 ? false : _b, _c = _a.dragDirectionLock, dragDirectionLock = _c === void 0 ? false : _c, _d = _a.dragPropagation, dragPropagation = _d === void 0 ? false : _d, _e = _a.dragConstraints, dragConstraints = _e === void 0 ? false : _e, _f = _a.dragElastic, dragElastic = _f === void 0 ? defaultElastic : _f, _g = _a.dragMomentum, dragMomentum = _g === void 0 ? true : _g, remainingProps = __rest(_a, ["drag", "dragDirectionLock", "dragPropagation", "dragConstraints", "dragElastic", "dragMomentum"]); this.props = Object.assign({ drag: drag, dragDirectionLock: dragDirectionLock, dragPropagation: dragPropagation, dragConstraints: dragConstraints, dragElastic: dragElastic, dragMomentum: dragMomentum }, remainingProps); }; /** * 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. * - If the component will perform layout animations, we output the gesture to the component's * visual bounding box * - Otherwise, we apply the delta to the x/y motion values. */ VisualElementDragControls.prototype.getAxisMotionValue = function (axis) { var _a = this.props, layout = _a.layout, layoutId = _a.layoutId; var dragKey = "_drag" + axis.toUpperCase(); if (this.props[dragKey]) { return this.props[dragKey]; } else if (!layout && layoutId === undefined) { return this.visualElement.getValue(axis, 0); } }; VisualElementDragControls.prototype.isLayoutDrag = function () { return !this.getAxisMotionValue("x"); }; VisualElementDragControls.prototype.isExternalDrag = function () { var _a = this.props, _dragX = _a._dragX, _dragY = _a._dragY; return _dragX || _dragY; }; VisualElementDragControls.prototype.animateDragEnd = function (velocity) { var _this = this; var _a = this.props, drag = _a.drag, dragMomentum = _a.dragMomentum, dragElastic = _a.dragElastic, dragTransition = _a.dragTransition; /** * Everything beyond the drag gesture should be performed with * relative projection so children stay in sync with their parent element. */ var isRelative = convertToRelativeProjection(this.visualElement, this.isLayoutDrag() && !this.isExternalDrag()); /** * If we had previously resolved constraints relative to the viewport, * we need to also convert those to a relative coordinate space for the animation */ var constraints = this.constraints || {}; if (isRelative && Object.keys(constraints).length && this.isLayoutDrag()) { var projectionParent = this.visualElement.getProjectionParent(); if (projectionParent) { var relativeConstraints_1 = calcRelativeOffset(projectionParent.projection.targetFinal, constraints); eachAxis(function (axis) { var _a = relativeConstraints_1[axis], min = _a.min, max = _a.max; constraints[axis] = { min: isNaN(min) ? undefined : min, max: isNaN(max) ? undefined : max, }; }); } } 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 : {}; /** * 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 = Object.assign(Object.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.getAxisMotionValue(axis) ? _this.startAxisValueAnimation(axis, inertia) : _this.visualElement.startLayoutAnimation(axis, inertia, isRelative); }); // Run all animations and then resolve the new drag constraints. return Promise.all(momentumAnimations).then(function () { var _a, _b; (_b = (_a = _this.props).onDragTransitionEnd) === null || _b === void 0 ? void 0 : _b.call(_a); }); }; VisualElementDragControls.prototype.stopMotion = function () { var _this = this; eachAxis(function (axis) { var axisValue = _this.getAxisMotionValue(axis); axisValue ? axisValue.stop() : _this.visualElement.stopLayoutAnimation(); }); }; VisualElementDragControls.prototype.startAxisValueAnimation = function (axis, transition) { var axisValue = this.getAxisMotionValue(axis); if (!axisValue) return; var currentValue = axisValue.get(); axisValue.set(currentValue); axisValue.set(currentValue); // Set twice to hard-reset velocity return startAnimation(axis, axisValue, 0, transition); }; VisualElementDragControls.prototype.scalePoint = function () { var _this = this; var _a = this.props, drag = _a.drag, dragConstraints = _a.dragConstraints; if (!isRefObject(dragConstraints) || !this.constraintsBox) return; // Stop any current animations as there can be some visual glitching if we resize mid animation this.stopMotion(); // Record the relative progress of the targetBox relative to the constraintsBox var boxProgress = { x: 0, y: 0 }; eachAxis(function (axis) { boxProgress[axis] = calcOrigin(_this.visualElement.projection.target[axis], _this.constraintsBox[axis]); }); /** * For each axis, calculate the current progress of the layout axis within the constraints. * Then, using the latest layout and constraints measurements, reposition the new layout axis * proportionally within the constraints. */ this.updateConstraints(function () { eachAxis(function (axis) { if (!shouldDrag(axis, drag, null)) return; // Calculate the position of the targetBox relative to the constraintsBox using the // previously calculated progress var _a = calcPositionFromProgress(_this.visualElement.projection.target[axis], _this.constraintsBox[axis], boxProgress[axis]), min = _a.min, max = _a.max; _this.visualElement.setProjectionTargetAxis(axis, min, max); }); }); /** * If any other draggable components are queuing the same tasks synchronously * this will wait until they've all been scheduled before flushing. */ setTimeout(flushLayout, 1); }; VisualElementDragControls.prototype.updateConstraints = function (onReady) { var _this = this; this.cancelLayout = batchLayout(function (read, write) { var ancestors = collectProjectingAncestors(_this.visualElement); write(function () { return ancestors.forEach(function (element) { return element.resetTransform(); }); }); read(function () { return updateLayoutMeasurement(_this.visualElement); }); write(function () { return ancestors.forEach(function (element) { return element.restoreTransform(); }); }); read(function () { _this.resolveDragConstraints(); }); if (onReady) write(onReady); }); }; VisualElementDragControls.prototype.mount = function (visualElement) { var _this = this; var element = 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.props, drag = _a.drag, _b = _a.dragListener, dragListener = _b === void 0 ? true : _b; drag && dragListener && _this.start(event); }); /** * 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.scalePoint(); }); /** * Ensure drag constraints are resolved correctly relative to the dragging element * whenever its layout changes. */ var stopLayoutUpdateListener = visualElement.onLayoutUpdate(function () { if (_this.isDragging) { _this.resolveDragConstraints(); } }); /** * If the previous component with this same layoutId was dragging at the time * it was unmounted, we want to continue the same gesture on this component. */ var prevDragCursor = visualElement.prevDragCursor; if (prevDragCursor) { this.start(lastPointerEvent, { cursorProgress: prevDragCursor }); } /** * Return a function that will teardown the drag gesture */ return function () { stopPointerListener === null || stopPointerListener === void 0 ? void 0 : stopPointerListener(); stopResizeListener === null || stopResizeListener === void 0 ? void 0 : stopResizeListener(); stopLayoutUpdateListener === null || stopLayoutUpdateListener === void 0 ? void 0 : stopLayoutUpdateListener(); _this.cancelDrag(); }; }; 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 };