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
JavaScript
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