vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
181 lines (134 loc) • 4.4 kB
text/typescript
import { Timeline } from '@/components/Timeline';
import { isFiniteNumber } from '@/internal/isFiniteNumber';
import { ISwipeMatrix, ISwipeVelocity } from '../global';
import type { Swipe } from '..';
const VELOCITIES_COUNT = 4;
export class SwipeInertia {
constructor(private _ctx: Swipe) {}
/** Inertia animation */
private _timeline?: Timeline;
/** Velocity tracking */
private _velocities: ISwipeVelocity[] = [];
/**
* Add new velocity sample
*/
public addVelocity(velocity: ISwipeVelocity) {
if (this.has) {
return;
}
this._velocities.push(velocity);
if (this._velocities.length > VELOCITIES_COUNT) {
this._velocities.shift();
}
}
/** Update last timestamp */
public updateLastTimestamp() {
const velocities = this._velocities;
const { length } = velocities;
if (length > 0) {
velocities[length - 1].timestamp = performance.now();
}
}
/** Returns current velocity */
public get velocity(): ISwipeMatrix {
const samples = this._velocities;
if (samples.length < 2) {
return { x: 0, y: 0, angle: 0 };
}
let totalWeight = 0;
let wvx = 0;
let wvy = 0;
let wva = 0;
for (let i = 1; i < samples.length; i += 1) {
const current = samples[i];
const previous = samples[i - 1];
const deltaX = current.x - previous.x;
const deltaY = current.y - previous.y;
let angleDiff = current.angle - previous.angle;
if (angleDiff > 180) angleDiff -= 360;
if (angleDiff < -180) angleDiff += 360;
const deltatTime = Math.max(current.timestamp - previous.timestamp, 1);
const sx = (deltaX / deltatTime) * 1000;
const sy = (deltaY / deltatTime) * 1000;
const sa = (angleDiff / deltatTime) * 1000;
const weight = 1 / Math.exp(-deltatTime * 0.1);
wvx += sx * weight;
wvy += sy * weight;
wva += sa * weight;
totalWeight += weight;
}
if (totalWeight > 0) {
return {
x: wvx / totalWeight,
y: wvy / totalWeight,
angle: wva / totalWeight,
};
}
return { x: 0, y: 0, angle: 0 };
}
/** Check if inertia is active */
get has() {
return !!this._timeline;
}
/** Apply inertia-based movement */
public release(onUpdate: (matrix: ISwipeMatrix) => void) {
const swipe = this._ctx;
const { props, callbacks } = swipe;
const { inertiaRatio: ratio, velocityModifier } = props;
const rawVelocity = this.velocity;
const sourceVelocity = {
x: rawVelocity.x * ratio,
y: rawVelocity.y * ratio,
angle: rawVelocity.angle * ratio,
};
const finalVelocity = velocityModifier
? velocityModifier(sourceVelocity)
: sourceVelocity;
const { x: velocityX, y: velocityY, angle: velocityA } = finalVelocity;
const distance = Math.sqrt(velocityX ** 2 + velocityY ** 2);
// Check if we have sufficient velocity
if (distance < props.inertiaDistanceThreshold) {
callbacks.emit('inertiaFail', undefined);
return;
}
// Calculate animation duration
const duration = props.inertiaDuration(distance);
// Check if the animation duration is positive
if (!isFiniteNumber(duration) || duration <= 0) {
callbacks.emit('inertiaFail', undefined);
return;
}
// Calculate the start and add matrices
const addMatrix = { x: 0, y: 0, angle: 0 };
// Start the inertia animation
this._timeline = new Timeline({ duration, easing: props.inertiaEasing });
this._timeline.on('start', () => callbacks.emit('inertiaStart', undefined));
this._timeline.on('update', ({ eased }) => {
addMatrix.x = velocityX * eased;
addMatrix.y = velocityY * eased;
addMatrix.angle = velocityA * eased;
onUpdate(addMatrix);
callbacks.emit('inertia', undefined);
});
this._timeline.on('end', () => {
this.cancel();
callbacks.emit('inertiaEnd', undefined);
});
setTimeout(() => this._timeline?.play(), 0);
}
/** Destroy inertia animation */
public cancel() {
if (!this._timeline) {
return;
}
if (this._timeline.progress < 1) {
this._ctx.callbacks.emit('inertiaCancel', undefined);
}
this._timeline?.destroy();
this._timeline = undefined;
}
/** Destroy instance */
public destroy() {
this._timeline?.destroy();
}
}