UNPKG

vevet

Version:

Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.

435 lines 14.4 kB
import { Module } from '../../base'; import { initVevet } from '../../global/initVevet'; import { addEventListener, EaseOutCubic } from '../../utils'; import { Pointers } from '../Pointers'; import { Timeline } from '../Timeline'; import { SwipeCoords } from './Coords'; import { SwipeInertia } from './Inertia'; import { MUTABLE_PROPS, STATIC_PROPS } from './props'; import { SwipeStyles } from './Styles'; export * from './types'; export * from './global'; /** * Manages swipe interactions: * - Tracks movement and detects direction * - Emits events on start, move, and end * - Supports exponential inertia * - Optional bounds with rubber-band overflow and bounce-back * * Notes: * - Does not transform elements, only computes coordinates. * * [Documentation](https://vevetjs.com/docs/Swipe) * * @group Components */ export class Swipe extends Module { /** * Returns default static properties. */ _getStatic() { return Object.assign(Object.assign({}, super._getStatic()), STATIC_PROPS); } /** * Returns default mutable properties. */ _getMutable() { return Object.assign(Object.assign({}, super._getMutable()), MUTABLE_PROPS); } constructor(props, onCallbacks) { super(props, onCallbacks); /** If swiping has started */ this._isSwiping = false; /** If swiping has been aborted */ this._isAborted = false; const { container, thumb, buttons, pointers } = this.props; this._coords = new SwipeCoords({ container, props: this.props, hasInertia: () => this.hasInertia, recalculateBoundsOnInertia: () => this.props.recalculateBoundsOnInertia, }); this._styles = new SwipeStyles(this); this._inertia = new SwipeInertia({ props: this.props, coords: this._coords, onStart: () => { this._coords.syncTempAngle(); this.callbacks.emit('inertiaStart', undefined); }, onFail: () => this.callbacks.emit('inertiaFail', undefined), onCancel: () => this.callbacks.emit('inertiaCancel', undefined), onEnd: () => this.callbacks.emit('inertiaEnd', undefined), }); // create pointers this._pointers = new Pointers({ container: thumb || container, buttons, minPointers: pointers, maxPointers: pointers, relative: false, enabled: this.props.enabled, disableUserSelect: this.props.disableUserSelect, }); // Set Events this._setEvents(); } /** Full coordinate snapshot (pointer space + `movement`). */ get coords() { return this._coords.coords; } /** Coordinate reference element. */ get container() { return this.props.container; } /** Whether release inertia is running. */ get hasInertia() { return this._inertia.has; } /** Whether overflow bounce-back timeline is running. */ get hasBounce() { return !!this._bounceTm; } /** Whether a swipe gesture is in progress. */ get isSwiping() { return this._isSwiping; } /** Handles property updates */ _handleProps(props) { super._handleProps(props); this._pointers.updateProps({ enabled: this.props.enabled }); this._styles.setInline(); if (!this.props.inertia || !this.props.enabled) { this.cancelInertia(); } if (!this.props.enabled) { this.cancelBounce(); } } /** Sets event listeners */ _setEvents() { const { callbacks } = this; const { container } = this.props; this._pointers.on('start', () => this._handlePointersStart()); this._pointers.on('pointerdown', (data) => callbacks.emit('pointerdown', data)); this._pointers.on('pointermove', (data) => callbacks.emit('pointermove', data)); this._pointers.on('pointerup', (data) => callbacks.emit('pointerup', data)); this._pointers.on('end', () => this._handlePointersEnd()); const touchstart = addEventListener(container, 'touchstart', (event) => this._handleTouchStart(event), { passive: false }); this.onDestroy(() => touchstart()); } /** Handles `touchstart` events */ _handleTouchStart(event) { if (!this.props.enabled) { return; } this._preventEdgeSwipe(event); this.callbacks.emit('touchstart', event); } /** Prevents edge swipes if enabled */ _preventEdgeSwipe(event) { const { props } = this; if (!props.preventEdgeSwipe) { return; } const threshold = props.edgeSwipeThreshold; const x = event.targetTouches[0].pageX; const shouldPrevent = x <= threshold || x >= initVevet().width - threshold; if (event.cancelable && shouldPrevent) { event.preventDefault(); this.callbacks.emit('preventEdgeSwipe', undefined); } } /** Handles pointers start */ _handlePointersStart() { this.cancelBounce(); this.cancelInertia(); const touchmove = addEventListener(window, 'touchmove', this._handleTouchMove.bind(this), { passive: false }); const mousemove = addEventListener(window, 'mousemove', this._handleMouseMove.bind(this)); const end = this._pointers.on('end', () => { this._handleEnd(); end(); touchmove(); mousemove(); }); this.onDestroy(() => { end(); touchmove(); mousemove(); }); } /** Handles pointers end */ _handlePointersEnd() { if (!this._isSwiping) { this.releaseBounce(); } } /** Handles `touchmove` event */ _handleTouchMove(event) { this.callbacks.emit('touchmove', event); if (this._isSwiping && this.props.preventTouchMove && event.cancelable) { event.preventDefault(); } this._handleMove('touch'); } /** Handles `mousemove` event */ _handleMouseMove(event) { if (this.props.requireCtrlKey && !event.ctrlKey) { return; } this.callbacks.emit('mousemove', event); this._handleMove('mouse'); } /** Handles move events */ _handleMove(type) { if (!this._pointers.move || !this.props.enabled) { return; } const data = this._coords; const state = data.decode(this._pointers.move.center); if (this._isAborted) { return; } // Save start coordinates if (!this._startCoord) { this._startCoord = Object.assign({}, state); } // Update start time if (!this._startTime) { this._startTime = +Date.now(); } // check if can start if (!this._isSwiping && !this._canStart(state, type)) { return; } // start if (!this._isSwiping) { this.cancelInertia(); this.cancelBounce(); this._isSwiping = true; this._startCoord = Object.assign({}, state); data.setStart(state); this.callbacks.emit('start', this.coords); this._styles.append(); } // move this._move(state); } /** Checks if swipe can start */ _canStart(state, type) { const { _startCoord: startCoord, _startTime: startTime } = this; if (!startCoord || !startTime) { return false; } const { threshold, minTime, axis, willAbort } = this.props; const diff = { x: state.x - startCoord.x, y: state.y - startCoord.y, }; // check threshold const distX = diff.x; const distY = diff.y; const dist = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2)); if (dist < threshold) { return false; } // check time if (+new Date() - startTime < minTime) { return false; } // check axis if (axis) { const rawAngle = (Math.atan2(Math.abs(diff.y), Math.abs(diff.x)) * 180) / Math.PI; const normalizedAngle = axis === 'x' ? rawAngle : 90 - rawAngle; if (normalizedAngle > 45) { this._reset(); this._isAborted = true; this.callbacks.emit('abort', undefined); return false; } } // Check if should abort const shouldAbort = willAbort({ type, state, start: startCoord, diff, }); if (shouldAbort) { this._reset(); this._isAborted = true; this.callbacks.emit('abort', undefined); return false; } return true; } /** Handles move events */ _move(state, applyRatio = true) { const coords = this._coords; // Update coords coords.update(state, applyRatio); // trigger callbacks this.callbacks.emit('move', this.coords); } /** Handles swipe end */ _handleEnd() { // reset this._startTime = undefined; this._isAborted = false; // check swiping if (!this.isSwiping) { return; } // reset this._reset(); // reset styles this._styles.remove(); // calculate direction const { x: diffX, y: diffY } = this._coords.diff; const absDiffX = Math.abs(diffX); const absDiffY = Math.abs(diffY); const { directionThreshold } = this.props; const endAxis = absDiffX > absDiffY ? 'x' : 'y'; if (endAxis === 'x' && absDiffX > directionThreshold) { if (diffX > 0) { this.callbacks.emit('toRight', undefined); } else if (diffX < 0) { this.callbacks.emit('toLeft', undefined); } } if (endAxis === 'y' && absDiffY > directionThreshold) { if (diffY > 0) { this.callbacks.emit('toBottom', undefined); } else if (diffY < 0) { this.callbacks.emit('toTop', undefined); } } // end callback this.callbacks.emit('end', this.coords); // end with inertia or bounce let hasInertia = false; if (this.props.inertia) { hasInertia = this._releaseInertia(); } if (!hasInertia) { this.releaseBounce(); } } /** Reset swipe states */ _reset() { this._startCoord = undefined; this._isSwiping = false; } /** Apply inertia-based movement */ _releaseInertia() { return this._inertia.release(({ x, y, angle }) => { this.callbacks.emit('inertia', undefined); this._move({ x, y, angle, time: performance.now() }, false); }); } /** Apply bounce overflow animation */ releaseBounce(targetDuration) { this.cancelBounce(); const { exceeds } = this._coords; const canBounce = this.props.canBounce(); if (!exceeds || (!exceeds.x && !exceeds.y && !exceeds.angle) || !canBounce) { return; } const start = Object.assign({}, this.current); const duration = targetDuration !== null && targetDuration !== void 0 ? targetDuration : this.props.bounceDuration; const tm = new Timeline({ duration, easing: EaseOutCubic }); this._bounceTm = tm; this._coords.syncTempAngle(); tm.on('update', ({ eased }) => { this._move({ x: start.x - exceeds.x * eased, y: start.y - exceeds.y * eased, angle: start.angle - exceeds.angle * eased, time: performance.now(), }, false); }); tm.on('end', this.cancelBounce.bind(this)); tm.play(); } /** Cancel inertia */ cancelInertia() { this._inertia.cancel(); } /** Cancel bounce */ cancelBounce() { var _a; (_a = this._bounceTm) === null || _a === void 0 ? void 0 : _a.destroy(); this._bounceTm = undefined; } /** Calculate swipe bounds */ calculateBounds() { return this._coords.calculateBounds(); } /** Pointer position at swipe start. */ get start() { return this._coords.start; } /** Previous pointer position. */ get prev() { return this._coords.prev; } /** Current pointer position. */ get current() { return this._coords.current; } /** Offset from swipe start to current pointer position. */ get diff() { return this._coords.diff; } /** Offset from previous to current pointer position. */ get step() { return this._coords.step; } /** Absolute path length since swipe start. */ get accum() { return this._coords.accum; } /** Total displacement in movement space (use for element transforms). */ get movement() { return this._coords.movement; } /** Current scale modifier. */ get scale() { return this._coords.scale; } /** * Sets programmatic scale in movement space. * Optionally zooms toward an origin point and emits `move`. */ setScale(value, origin) { this._coords.applyScale(value, origin); this._move(Object.assign(Object.assign({}, this.current), { time: performance.now() })); if (!this._inertia.has) { this.releaseBounce(0); } } /** * Sets programmatic displacement in movement space. * Reapplies rubber, snap, emits `move`, and cancels overflow bounce. */ setMovement(value) { this._coords.movement = value; this._move(Object.assign(Object.assign({}, this.current), { time: performance.now() })); this.releaseBounce(0); } /** * Destroys the component */ _destroy() { super._destroy(); this.cancelBounce(); this._pointers.destroy(); this._inertia.destroy(); this._styles.remove(); } } //# sourceMappingURL=index.js.map