vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
381 lines • 13.8 kB
JavaScript
import { Module } from '../../base';
import { Timeline } from '../Timeline';
import { Raf } from '../Raf';
import { onResize, scoped, loop, clamp, EaseOutCubic, damp, lerp, toPixels, } from '../../utils';
import { SnapSlide } from './Slide';
import { SnapWheel } from './Wheel';
import { SnapSwipe } from './Swipe';
import { SnapTrack } from './Track';
import { SnapKeyboard } from './Keyboard';
import { initVevet } from '../../global/initVevet';
export * from './types';
export * from './Slide';
// todo: jsdoc
/**
* Snap/Carousel handler.
* This class manages sliding progress with options like swipe, wheel interactions, and smooth transitions.
*
* Please not that the class does not apply any styles to the slides, it only handles the logic.
*
* [Documentation](https://antonbobrov.github.io/vevet/docs/components/Snap)
*
* @group Components
*/
export class Snap extends Module {
/** Retrieves the default static properties. */
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), { activeIndex: 0 });
}
/** Retrieves the default mutable properties. */
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), { slides: false, direction: 'horizontal', centered: false, loop: false, gap: 0, lerp: 0.2, freemode: false, stickOnResize: true, friction: 0.15, edgeFriction: 0.85, duration: 500, easing: EaseOutCubic, swipe: true, grabCursor: false, swipeSpeed: 1, swipeAxis: 'auto', followSwipe: true, shortSwipes: true, shortSwipesDuration: 300, shortSwipesThreshold: 30, swipeFriction: false, swipeLerp: initVevet().mobile ? 1 : 0.6, swipeThreshold: 5, swipeMinTime: 0, wheel: false, wheelSpeed: 1, wheelAxis: 'auto', followWheel: true });
}
constructor(props) {
super(props);
/** Container size */
this._domSize = 0;
/** All slides */
this._slides = [];
const { container, activeIndex } = this.props;
// set vars
this._activeIndex = activeIndex;
// add resize event
this._resizeHandler = onResize({
element: container,
callback: () => this._handleResize(),
name: this.name,
});
// initial resize
this._resizeHandler.debounceResize();
// Create the animation frame
this._raf = new Raf();
this._raf.on('frame', () => this._handleRaf());
this._raf.on('play', () => this._callbacks.emit('rafPlay', undefined));
this._raf.on('pause', () => this._callbacks.emit('rafPause', undefined));
// fetch slides
this._fetchSlides();
// add wheel listener
this._wheel = new SnapWheel(this);
// add swipe
this._swipe = new SnapSwipe(this);
// add track
this._track = new SnapTrack(this);
// add keyboard
this._keyboard = new SnapKeyboard(this);
}
/** Handles properties change */
_handleProps() {
// attach slides
this._fetchSlides();
// resize instantly
this._resizeHandler.resize();
// update props
super._handleProps();
}
/** Update slides list and attach them */
_fetchSlides() {
this._slides.forEach((slide) => slide.detach());
const children = this.props.slides
? this.props.slides
: Array.from(this.props.container.children);
this._slides = children.map((item) => {
if (item instanceof SnapSlide) {
return item;
}
return new SnapSlide(item);
});
this._slides.forEach((slide, index) => slide.attach(this, index));
}
/** Request resize (handled with debounce timeout) */
resize(isManual = false) {
if (isManual) {
this._resizeHandler.resize();
}
else {
this._resizeHandler.debounceResize();
}
}
/** Resize the scene and reflow */
_handleResize() {
const { direction, container } = this.props;
// cancel sticky behavior
this.cancelTransition();
// update container size
this._domSize =
direction === 'horizontal'
? container.offsetWidth
: container.offsetHeight;
// reflow
this._reflow();
// emit callbacks
this.callbacks.emit('resize', undefined);
}
/** Get container */
get container() {
return this.props.container;
}
/** Container size depending on direction (width or height) */
get domSize() {
return this._domSize;
}
/** All slides */
get slides() {
return this._slides;
}
/** Active slide index */
get activeIndex() {
return this._activeIndex;
}
/** Active slide */
get activeSlide() {
return this.slides[this._activeIndex];
}
get isEmpty() {
return this.slides.length === 0;
}
/** Get axis name depending on direction */
get axis() {
return this.props.direction === 'horizontal' ? 'x' : 'y';
}
/** Snap track */
get track() {
return this._track;
}
/** If transition in progress */
get isTransitioning() {
return !!this._timeline;
}
/** Reflow: update static values of slides */
_reflow() {
const { slides, props } = this;
if (slides.length === 0) {
return;
}
// Calculate static values
slides.reduce((prev, slide) => {
slide.setStaticCoord(prev);
return prev + slide.size + toPixels(props.gap);
}, 0);
// Reset to active slide
const slide = slides.find(({ index }) => index === this.activeIndex);
if (props.stickOnResize && slide) {
this._track.set(slide.staticCoord);
}
else {
this._track.clampTarget();
}
// Emit callbacks
this.callbacks.emit('reflow', undefined);
// Render after resize
this._render();
}
/** Handle RAF update, interpolate track values */
_handleRaf() {
if (this.isTransitioning) {
return;
}
const { _track: track, props, _swipe: swipe } = this;
// Get lerp factor
const lerpFactor = swipe.isSwiping && props.swipeLerp ? props.swipeLerp : props.lerp;
// Interpolate track value
track.lerp(this._raf.lerpFactor(lerpFactor));
// Stop raf if target reached
if (track.isInterpolated) {
this._raf.pause();
}
// Render the scene
this._render(this._raf.duration);
}
/** Render slides logic */
_render(frameDuration = 0) {
if (this.isEmpty) {
return;
}
const { _track: track, props } = this;
// Update values
this._updateSlidesCoords();
this._updateSlidesProgress();
// Get magnet after slide coordinates are updated
const { magnet, _swipe: swipe } = this;
if (!magnet) {
return;
}
// Active index change
if (magnet.slide.index !== this._activeIndex) {
this._activeIndex = magnet.slide.index;
this.callbacks.emit('activeSlide', this.activeSlide);
}
// Check if friction is allowed
const canHaveFriction = (swipe.isSwiping && swipe.allowFriction) || !swipe.isSwiping;
// Apply friction
if (!track.isSlideScrolling &&
!props.freemode &&
canHaveFriction &&
frameDuration > 0 &&
props.friction >= 0) {
track.target = damp(track.target, track.current + magnet.magnetDiff, props.friction * props.lerp, frameDuration, 0.000001);
}
// Render slides
this.slides.forEach((slide) => slide.render());
// Emit Calbacks
this.callbacks.emit('update', undefined);
}
/** Update slides values */
_updateSlidesCoords() {
const { slides, domSize, _track: track } = this;
const { centered: isCentered } = this.props;
slides.forEach((slide) => {
const { staticCoord, size } = slide;
const offset = isCentered ? domSize / 2 - slides[0].size / 2 : 0;
if (!track.canLoop) {
slide.setCoord(staticCoord + offset - track.current);
return;
}
if (isCentered) {
slide.setCoord(loop(staticCoord + offset - track.current, -track.max / 2 + offset, track.max / 2 + offset));
return;
}
slide.setCoord(loop(staticCoord - track.current, -size, track.max - size));
});
}
/** Update slides progress */
_updateSlidesProgress() {
const { slides, domSize } = this;
slides.forEach((slide) => {
const { coord, size } = slide;
if (this.props.centered) {
const center = domSize / 2 - size / 2;
slide.setProgress(scoped(coord, center, center - size));
return;
}
slide.setProgress(scoped(coord, 0, -size));
});
}
/** Get slide magnets */
get magnets() {
const { domSize, props, track } = this;
const threshold = props.centered ? domSize / 2 : 0;
const slideMagnets = this.slides.map((slide) => {
const firstPoint = props.centered
? slide.coord + slide.size / 2
: slide.coord;
const points = [firstPoint];
if (slide.size > domSize) {
if (props.centered) {
points.push(firstPoint + (domSize - slide.size) / 2);
points.push(firstPoint - (domSize - slide.size) / 2);
}
else {
points.push(firstPoint + slide.size - domSize);
}
}
return points.map((point) => ({
slide,
magnetCoord: point,
magnetDiff: point - threshold,
staticMagnetCoord: point + this._track.current,
}));
});
const flatMagnets = slideMagnets.flat();
if (!track.canLoop && !props.centered) {
flatMagnets.forEach((magnet) => {
const oldStaticMagnetCoord = magnet.staticMagnetCoord;
const newStaticMagnetCoord = clamp(magnet.staticMagnetCoord, 0, this._track.max);
const diff = oldStaticMagnetCoord - newStaticMagnetCoord;
magnet.staticMagnetCoord = newStaticMagnetCoord;
magnet.magnetCoord -= diff;
magnet.magnetDiff -= diff;
});
}
return flatMagnets;
}
/** Get nearest magnet */
get magnet() {
if (this.isEmpty) {
return undefined;
}
const magnets = this.magnets.sort((a, b) => Math.abs(a.magnetDiff) - Math.abs(b.magnetDiff));
return magnets[0];
}
/** Cancel sticky behavior */
cancelTransition() {
var _a;
(_a = this._timeline) === null || _a === void 0 ? void 0 : _a.destroy();
this._timeline = undefined;
}
/** Stick to the nearest magnet */
stick() {
const { magnet } = this;
if (this.track.isSlideScrolling || !magnet) {
return;
}
this.toCoord(this._track.current + magnet.magnetDiff);
}
/** Go to a definite coordinate */
toCoord(coordinate, duration = this.props.duration) {
if (this.isEmpty) {
return;
}
this.cancelTransition();
const start = this._track.current;
const end = coordinate;
const diff = Math.abs(end - start);
const tm = new Timeline({
duration: typeof duration === 'number' ? duration : duration(diff),
easing: this.props.easing,
});
this._timeline = tm;
tm.on('start', () => this.callbacks.emit('timelineStart', undefined));
tm.on('update', (data) => {
this._track.current = lerp(start, end, data.eased);
this._track.target = this._track.current;
this._render();
this.callbacks.emit('timelineUpdate', data);
});
tm.on('end', () => {
tm.destroy();
this.callbacks.emit('timelineEnd', undefined);
this._timeline = undefined;
});
tm.play();
}
/** Go to a slide by index */
toSlide(index, duration = this.props.duration) {
if (index === this.activeIndex) {
this.stick();
return;
}
const magnet = this.magnets.find((data) => data.slide.index === index);
if (!magnet) {
return;
}
this.toCoord(this._track.current + magnet.magnetDiff, duration);
}
/** Go to next slide */
next(duration = this.props.duration) {
const { props, slides, activeIndex } = this;
const index = props.loop
? loop(activeIndex + 1, 0, slides.length)
: Math.min(activeIndex + 1, slides.length - 1);
this.toSlide(index, duration);
}
/** Go to previous slide */
prev(duration = this.props.duration) {
const { props, slides, activeIndex } = this;
const index = props.loop
? loop(activeIndex - 1, 0, slides.length)
: Math.max(activeIndex - 1, 0);
this.toSlide(index, duration);
}
/**
* Destroys the component and clears all timeouts and resources.
*/
_destroy() {
super._destroy();
this._resizeHandler.remove();
this.cancelTransition();
this._raf.destroy();
this._slides.forEach((slide) => slide.detach());
}
}
//# sourceMappingURL=index.js.map