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.

291 lines 10.1 kB
import { Raf } from '../../../components/Raf'; import { Timeline } from '../../../components/Timeline'; import { isNumber } from '../../../internal/isNumber'; import { onlyFinite } from '../../../internal/onlyFinite'; import { toPixels } from '../../../utils'; import { clamp, lerp, loop } from '../../../utils/math'; import { LERP_APPROXIMATION } from '../props'; export class SnapTrack { constructor(props, _slides, ctx) { this.props = props; this._slides = _slides; this.ctx = ctx; /** Interpolation impulse */ this._impulse = { current: 0, target: 0 }; /** The current track value */ this._current = 0; /** The target track value */ this._target = 0; /** Whether the track is destroyed */ this._isDestroyed = false; // Create the animation frame this._raf = new Raf(); this._raf.on('frame', () => this._handleRaf()); this._raf.on('play', () => { var _a; return (_a = ctx.onRafPlay) === null || _a === void 0 ? void 0 : _a.call(ctx); }); this._raf.on('pause', () => { var _a; return (_a = ctx.onRafPause) === null || _a === void 0 ? void 0 : _a.call(ctx); }); } get slides() { return this._slides(); } /** Gets the target slide index */ get targetIndex() { return this._targetIndex; } /** Gets the interpolation impulse */ get impulse() { return this._impulse.current; } /** Gets the current track value. */ get current() { return this._current; } /** Sets the current track value */ set current(value) { this._current = value; } /** Gets the target track value. */ get target() { return this._target; } /** Sets the target track value */ set target(value) { const containerSize = this.ctx.containerSize(); const diff = value - this._target; this._target = value; this._impulse.target += containerSize ? onlyFinite(diff / containerSize) : 0; this._impulse.target = clamp(this._impulse.target, -1, 1); } /** Detect if can loop */ get canLoop() { const { props, slides } = this; return props.loop && slides.length > 1; } /** Get looped current value */ get loopedCurrent() { return this.loopCoord(this.current); } /** Get track offset */ get offset() { const { ctx } = this; const origin = ctx.origin(); const containerSize = ctx.containerSize(); const firstSlideSize = ctx.firstSlideSize(); if (origin === 'center') { return containerSize / 2 - firstSlideSize / 2; } else if (origin === 'end') { return containerSize - firstSlideSize; } return 0; } /** Get loop count */ get loopCount() { if (!this.canLoop) { return 0; } return Math.floor(onlyFinite(this.current / this.max)); } /** If transition in progress */ get isTransitioning() { return !!this._tm; } /** Get minimum track value */ get min() { const containerSize = this.ctx.containerSize(); const origin = this.ctx.origin(); const firstSlide = this.slides[0]; if (this.canLoop) { return 0; } if (origin === 'center') { if (firstSlide.size > containerSize) { return containerSize / 2 - firstSlide.size / 2; } } if (origin === 'end') { if (firstSlide.size > containerSize) { return containerSize - firstSlide.size; } } return 0; } /** Get maximum track value */ get max() { const containerSize = this.ctx.containerSize(); const origin = this.ctx.origin(); const { slides, canLoop, props } = this; const firstSlide = slides[0]; const lastSlide = slides[slides.length - 1]; const lastCoordWithSlide = lastSlide.staticCoord + lastSlide.size; let max = canLoop ? lastCoordWithSlide + toPixels(props.gap) : lastCoordWithSlide - containerSize; if (canLoop) { return max; } if (origin === 'center') { max += containerSize / 2 - firstSlide.size / 2; if (lastSlide.size < containerSize) { max += containerSize / 2 - lastSlide.size / 2; } } if (origin === 'end') { max += containerSize - firstSlide.size; } if (origin === 'start') { max = Math.max(max, 0); } return max; } /** Get track progress. From 0 to 1 if not loop. From -Infinity to Infinity if loop */ get progress() { return onlyFinite(this.current / this.max); } /** If the start has been reached */ get isStart() { if (this.props.loop) { return false; } return Math.floor(this.target) <= Math.floor(this.min); } /** If the end has been reached */ get isEnd() { if (this.props.loop) { return false; } return Math.floor(this.target) >= Math.floor(this.max); } /** Whether the track is interpolated */ get isInterpolated() { return this.current === this.target && this._impulse.current === 0; } /** Handle RAF update, interpolate track values */ _handleRaf() { const { isTransitioning, props, _raf: raf } = this; if (isTransitioning) { return; } // Interpolate track value const ease = raf.lerpFactor(props.lerp); this._lerp(ease); // Stop raf if target reached if (this.isInterpolated) { raf.pause(); } // Render the scene this.ctx.onRender(raf.duration); } /** Awake requestAnimationFrame */ awake() { this._raf.play(); } /** Set track target value */ updateTarget(value) { this.target = value; this.awake(); } /** Clamp target value between min and max values */ clampTarget() { if (!this.canLoop) { this.target = clamp(this.target, this.min, this.max); } this.awake(); } /** Set a value to current & target value instantly */ set(value) { this.current = value; this.target = value; this._impulse.current = 0; this._impulse.target = 0; } /** Loop a coordinate if can loop */ loopCoord(coord) { return this.canLoop ? loop(coord, this.min, this.max) : coord; } /** Interpolate the current track value */ _lerp(initialFactor) { const { target, _impulse: impulse } = this; let lerpFactor = initialFactor; // Interpolate current value const rest = Math.abs(this.current - target); const fastThreshold = 3; if (rest < fastThreshold) { const fastProgress = 1 - rest / fastThreshold; const additionalFactor = (1 - lerpFactor) / 15; lerpFactor += additionalFactor * fastProgress; } this.current = lerp(this.current, target, lerpFactor, LERP_APPROXIMATION); // Interpolate impulse impulse.target = lerp(impulse.target, 0, lerpFactor, LERP_APPROXIMATION); impulse.current = lerp(impulse.current, impulse.target, lerpFactor, LERP_APPROXIMATION); } /** Go to a definite coordinate */ toCoord(coordinate, options) { var _a, _b; if (this._isDestroyed) { return false; } const { props, ctx } = this; this.cancelTransition(); const start = this.current; const end = coordinate; const diff = Math.abs(end - start); const durationProp = (_a = options === null || options === void 0 ? void 0 : options.duration) !== null && _a !== void 0 ? _a : props.duration; let duration = isNumber(durationProp) ? durationProp : durationProp(diff); if (diff === 0) { duration = 0; } const easing = (_b = options === null || options === void 0 ? void 0 : options.easing) !== null && _b !== void 0 ? _b : props.easing; const tm = new Timeline({ duration, easing }); this._tm = tm; tm.on('start', () => { var _a; ctx.onTimelineStart(); (_a = options === null || options === void 0 ? void 0 : options.onStart) === null || _a === void 0 ? void 0 : _a.call(options); }); tm.on('update', (data) => { var _a; this.current = lerp(start, end, data.eased); this.target = this.current; this._impulse.current = this._impulse.current * (1 - data.progress); this._impulse.target = this._impulse.current; if (data.progress === 1) { this.setTargetIndex(undefined); this._tm = undefined; } ctx.onTimelineUpdate(data); ctx.onRender(); (_a = options === null || options === void 0 ? void 0 : options.onUpdate) === null || _a === void 0 ? void 0 : _a.call(options, data); }); tm.on('end', () => { var _a; tm.destroy(); ctx.onTimelineEnd(); (_a = options === null || options === void 0 ? void 0 : options.onEnd) === null || _a === void 0 ? void 0 : _a.call(options); }); tm.on('destroy', () => { this.setTargetIndex(undefined); }); tm.play(); return true; } /** Set target index */ setTargetIndex(value) { this._targetIndex = value; } /** Cancel sticky behavior */ cancelTransition() { var _a; (_a = this._tm) === null || _a === void 0 ? void 0 : _a.destroy(); this._tm = undefined; } /** Destroy the instance */ destroy() { this._isDestroyed = true; this._raf.destroy(); this.cancelTransition(); } } //# sourceMappingURL=index.js.map