vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
358 lines (275 loc) • 8.41 kB
text/typescript
import { ISwipeCoords, ISwipeMatrix, Swipe } from '@/components/Swipe';
import { clamp } from '@/utils';
import { Snap } from '../..';
import { SnapLogic } from '../SnapLogic';
export class SnapSwipe extends SnapLogic {
/** Swipe events */
private swipe: Swipe;
/** Active index on swipe start */
private _startIndex: number;
/** Swipe start time */
private _startTime: number;
constructor(snap: Snap) {
super(snap);
this._startIndex = snap.activeIndex;
this._startTime = 0;
const swipe = new Swipe({
container: snap.eventsEmitter,
inertia: false,
velocityModifier: this._handleVelocityModifier.bind(this),
inertiaDistanceThreshold: 5,
...this.swipeProps,
});
this.swipe = swipe;
this.addDestructor(() => swipe.destroy());
swipe.on('start', (data) => this._handleSwipeStart(data));
swipe.on('move', (data) => this._handleSwipeMove(data));
swipe.on('end', (data) => this._handleSwipeEnd(data));
swipe.on('inertiaStart', () => this._handleSwipeInertiaStart());
swipe.on('inertiaEnd', () => this._handleSwipeInertiaEnd());
swipe.on('inertiaFail', () => this._handleSwipeInertiaFail());
swipe.on('inertiaCancel', () => this._handleSwipeInertiaCancel());
// handle props change
snap.on('props', () => swipe.updateProps(this.swipeProps), {
protected: true,
});
}
/** Check if swiping in action */
get isSwiping() {
return this.swipe.isSwiping;
}
/** Check if swipe has inertia */
get hasIntertia() {
return this.swipe.hasInertia;
}
/** Snap track */
private get track() {
// @ts-ignore
// eslint-disable-next-line no-underscore-dangle
return this.snap._track;
}
/** Axis name depending on swipe direction */
private get axis() {
const { props, axis } = this.snap;
return props.swipeAxis === 'auto' ? axis : props.swipeAxis;
}
/** Detect if swipe is short */
private get isShort() {
const { props } = this.snap;
if (!props.shortSwipes) {
return false;
}
const diff = +new Date() - this._startTime;
return diff <= props.shortSwipesDuration;
}
/** Checks if resistance is allowed */
get allowFriction() {
return !this.isShort && this.snap.props.swipeFriction;
}
/** Swipe difference between start and current coordinates */
private get diff() {
const { diff } = this.swipe;
const initialDiff = diff[this.axis];
return initialDiff / Math.abs(this.snap.props.swipeSpeed);
}
/** Get swipe properties */
private get swipeProps() {
const { props } = this.snap;
return {
enabled: props.swipe,
grabCursor: props.grabCursor,
minTime: props.swipeMinTime,
threshold: props.swipeThreshold,
axis: this.axis === 'angle' ? null : this.axis,
relative: this.axis === 'angle',
ratio: this.axis === 'angle' ? 1 : props.swipeSpeed,
inertiaDuration: props.swipeInertiaDuration,
inertiaRatio: props.swipeInertiaRatio,
};
}
/** Modify swipe velocity */
private _handleVelocityModifier(source: ISwipeMatrix) {
const { snap, track } = this;
const { coord: slideCoord, size: slideSize } = snap.activeSlide;
// Simple freemode
if (snap.props.freemode === true) {
return source;
}
// Update target coordinate
track.target = track.current;
// Sticky freemode
if (snap.props.freemode === 'sticky' && !snap.isSlideScrolling) {
const virtualCoord = track.loopedCurrent - source[this.axis];
const magnet = snap.getNearestMagnet(virtualCoord);
if (!magnet) {
return source;
}
const newVelocity = track.loopedCurrent - virtualCoord - magnet.diff;
return {
...source,
[this.axis]: newVelocity,
};
}
// Freemode: false, when slides are scrolled
const value = clamp(
source[this.axis],
-slideCoord,
snap.containerSize - slideSize - slideCoord,
);
const output = { ...source, [this.axis]: value };
return output;
}
/** Handles swipe `start` event */
private _handleSwipeStart(coords: ISwipeCoords) {
const { snap } = this;
this._startIndex = snap.activeIndex;
this._startTime = +new Date();
// disable pointer events
snap.eventsEmitter.style.pointerEvents = 'none';
// cancel sticky behavior
if (snap.props.followSwipe) {
snap.cancelTransition();
}
// Emit callbacks
snap.callbacks.emit('swipeStart', coords);
}
/** Handles swipe `move` event */
private _handleSwipeMove(coords: ISwipeCoords) {
const { snap, track, swipe, axis } = this;
const { props, callbacks } = snap;
const { followSwipe: shouldFollow } = props;
if (!shouldFollow && !snap.isSlideScrolling) {
return;
}
// Normalize swipe delta
let swipeDelta = coords.step[axis];
if (axis === 'angle') {
const trackLength = snap.max - snap.min;
swipeDelta = trackLength * (swipeDelta / 360);
}
const delta = swipeDelta * -1;
// Update track target
track.iterateTarget(delta);
// Clamp target if inertia is animating
if (swipe.hasInertia) {
track.clampTarget();
}
// Emit move callbacks
callbacks.emit('swipe', coords);
}
/** Handles swipe `end` event */
private _handleSwipeEnd(coords: ISwipeCoords) {
this._end();
// Enable pointer events
this.snap.eventsEmitter.style.pointerEvents = '';
// Emit end callbacks
this.snap.callbacks.emit('swipeEnd', coords);
}
/** Handles swipe inertia start */
private _handleSwipeInertiaStart() {
this.snap.callbacks.emit('swipeInertiaStart', undefined);
}
/** Handles swipe inertia end */
private _handleSwipeInertiaEnd() {
this.snap.callbacks.emit('swipeInertiaEnd', undefined);
}
/** Handles swipe inertia fail */
private _handleSwipeInertiaFail() {
const { snap } = this;
if (snap.props.freemode === 'sticky' && !snap.isSlideScrolling) {
if (this.isShort) {
this._endShort();
} else {
snap.stick();
}
} else {
this.snap.render();
}
this.snap.callbacks.emit('swipeInertiaFail', undefined);
}
/** Handles swipe inertia cancel */
private _handleSwipeInertiaCancel() {
this.snap.callbacks.emit('swipeInertiaCancel', undefined);
}
/** End swipe action */
private _end() {
const { snap, swipe, track } = this;
const { props } = snap;
// Handle freemode
if (props.freemode) {
swipe.updateProps({ inertia: true });
// Clamp & stick if out of bounds
if (
!track.canLoop &&
(track.target < track.min || track.target > track.max)
) {
swipe.cancelInertia();
snap.stick();
}
// End short swipe
if (this.isShort && props.freemode === 'sticky') {
swipe.updateProps({ inertia: false });
swipe.cancelInertia();
this._endShort();
}
return;
}
// Enable inertia if active slide is being scrolled
if (snap.isSlideScrolling) {
swipe.updateProps({ inertia: true });
return;
}
// Disable inertia
swipe.updateProps({ inertia: false });
// Return if followSwipe is disabled
if (!props.followSwipe) {
this._endNoFollow();
return;
}
// Short swipe
if (this.isShort) {
this._endShort();
return;
}
// Or just stick to the nearest slide
snap.stick();
}
/** End short swipe */
private _endShort() {
const { diff, snap } = this;
const { props, activeSlide } = snap;
if (Math.abs(diff) < props.shortSwipesThreshold) {
snap.stick();
return;
}
const normalizedDiff = Math.sign(diff);
if (this._startIndex !== snap.activeIndex) {
if (normalizedDiff < 0 && activeSlide.progress > 0) {
snap.next();
} else if (normalizedDiff > 0 && activeSlide.progress < 0) {
snap.prev();
} else {
snap.stick();
}
return;
}
if (normalizedDiff < 0) {
snap.next();
} else {
snap.prev();
}
}
/** End action when `followSwipe` is disabled */
private _endNoFollow() {
const { diff, snap } = this;
if (Math.abs(diff) < 20) {
snap.stick();
return;
}
if (diff < 0) {
snap.next();
} else {
snap.prev();
}
}
}