vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
233 lines • 9.86 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Raf } from '../../../components/Raf';
import { isFiniteNumber } from '../../../internal/isFiniteNumber';
import { clamp, lerp } from '../../../utils';
const IDLE_VEC3 = { x: 0, y: 0, angle: 0 };
const IDLE_STATE = Object.assign(Object.assign({}, IDLE_VEC3), { time: 0 });
const LERP_APPROX = 0.01;
const BELOW_THRESHOLD = 0.1;
export class SwipeInertia {
constructor(ctx) {
this.ctx = ctx;
this._velocity = Object.assign({}, IDLE_STATE);
this._initialVelocity = Object.assign({}, IDLE_STATE);
this._saveRawMovement = Object.assign({}, IDLE_VEC3);
this._rawMovement = Object.assign({}, IDLE_VEC3);
this._saveStep = Object.assign({}, IDLE_STATE);
this._saveCurrent = Object.assign({}, IDLE_STATE);
}
/** Check if inertia is active */
get has() {
return !!this._raf;
}
/** Apply inertia-based movement */
release(onUpdate) {
const { ctx } = this;
const { props } = ctx;
this._modifiedDistance = undefined;
this._saveCurrent = Object.assign({}, ctx.coords.current);
this._saveStep = Object.assign({}, ctx.coords.step);
this._saveRawMovement = Object.assign({}, ctx.coords.rawMovement);
this._rawMovement = Object.assign({}, ctx.coords.rawMovement);
const data = this._calcVelocity();
if (!data || !isFiniteNumber(data.dt) || data.dt <= 0) {
ctx.onFail();
return false;
}
const { linearSpeed, angularSpeed, vx, vy, va, threshold } = data;
if (!isFiniteNumber(linearSpeed) ||
!isFiniteNumber(angularSpeed) ||
(linearSpeed < threshold && angularSpeed < threshold)) {
ctx.onFail();
return false;
}
this._onUpdate = onUpdate;
this._velocity = { x: vx, y: vy, angle: va, time: performance.now() };
this._initialVelocity = Object.assign({}, this._velocity);
if (props.inertiaDistanceModifier) {
this._modifiedDistance = props.inertiaDistanceModifier({
x: this._predictDistance(vx, props.inertiaDecay),
y: this._predictDistance(vy, props.inertiaDecay),
angle: this._predictDistance(va, props.inertiaDecay),
});
}
this._raf = new Raf({
enabled: true,
onFrame: this._handleRaf.bind(this),
});
this.ctx.onStart();
return true;
}
/** Calculate velocity */
_calcVelocity() {
const { _saveCurrent: current, _saveStep: step } = this;
const _a = this.ctx.props, { inertiaRatio, ratio, maxVelocity } = _a, props = __rest(_a, ["inertiaRatio", "ratio", "maxVelocity"]);
if (!current || !step) {
return null;
}
const gap = performance.now() - current.time;
const dt = Math.max(step.time, gap, 1);
const iRatio = isFiniteNumber(inertiaRatio) ? inertiaRatio : 1;
const sRatio = isFiniteNumber(ratio) ? ratio : 1;
const finalRatio = sRatio * iRatio;
const maxVX = maxVelocity.x ? Math.abs(maxVelocity.x) : 0;
let vx = (step.x / dt) * finalRatio;
vx = clamp(vx, -maxVX, maxVX);
const maxVY = maxVelocity.y ? Math.abs(maxVelocity.y) : 0;
let vy = (step.y / dt) * finalRatio;
vy = clamp(vy, -maxVY, maxVY);
const maxVA = maxVelocity.angle ? Math.abs(maxVelocity.angle) : 0;
let va = (step.angle / dt) * finalRatio;
va = clamp(va, -maxVA, maxVA);
const linearSpeed = Math.hypot(vx, vy) * 1000;
const angularSpeed = Math.abs(va) * 1000;
const threshold = props.inertiaThreshold;
return { dt, vx, vy, va, linearSpeed, angularSpeed, threshold };
}
/** Handle RAF update */
_handleRaf() {
var _a;
if (!this._raf) {
return;
}
const { _raf: raf } = this;
const duration = this._raf.duration;
const { coords, props } = this.ctx;
const { _velocity: velocity, _saveCurrent: startCurrent, _saveRawMovement: startRawMovement, _rawMovement: rawMovement, _modifiedDistance: distance, _initialVelocity: initial, } = this;
const frameMs = duration;
// Delta
const dx = velocity.x * frameMs;
const dy = velocity.y * frameMs;
const dAngle = velocity.angle * frameMs;
// Friction
const frictionEase = raf.lerpFactor(props.inertiaDecay);
velocity.x = lerp(velocity.x, 0, frictionEase);
velocity.y = lerp(velocity.y, 0, frictionEase);
velocity.angle = lerp(velocity.angle, 0, frictionEase);
// Movement
if (distance) {
const xP = this._getVelocityProgress(velocity.x, initial.x);
const yP = this._getVelocityProgress(velocity.y, initial.y);
const aP = this._getVelocityProgress(velocity.angle, initial.angle);
rawMovement.x = startRawMovement.x + distance.x * xP;
rawMovement.y = startRawMovement.y + distance.y * yP;
rawMovement.angle = startRawMovement.angle + distance.angle * aP;
}
else {
rawMovement.x += dx;
rawMovement.y += dy;
rawMovement.angle += dAngle;
}
// Bounce
let isBouncing = false;
const rawBounceEase = props.inertiaBounceEase;
const bounceEase = rawBounceEase >= 1 ? 1 : raf.lerpFactor(rawBounceEase);
// Bounce within bounds
const { bounds } = coords;
if (bounds === null || bounds === void 0 ? void 0 : bounds.x) {
const bx = this._applyAxisBounce('x', rawMovement.x, velocity.x, bounds.x, bounceEase);
rawMovement.x = bx.value;
velocity.x = bx.velocity;
isBouncing = 'bounceFinished' in bx ? true : isBouncing;
}
if (bounds === null || bounds === void 0 ? void 0 : bounds.y) {
const by = this._applyAxisBounce('y', rawMovement.y, velocity.y, bounds.y, bounceEase);
rawMovement.y = by.value;
velocity.y = by.velocity;
isBouncing = 'bounceFinished' in by ? true : isBouncing;
}
if (bounds === null || bounds === void 0 ? void 0 : bounds.angle) {
const ba = this._applyAxisBounce('angle', rawMovement.angle, velocity.angle, bounds.angle, bounceEase);
rawMovement.angle = ba.value;
velocity.angle = ba.velocity;
isBouncing = 'bounceFinished' in ba ? true : isBouncing;
}
// Callbacks
const totalX = rawMovement.x - startRawMovement.x;
const totalY = rawMovement.y - startRawMovement.y;
const totalA = rawMovement.angle - startRawMovement.angle;
const x = startCurrent.x + totalX;
const y = startCurrent.y + totalY;
const angle = startCurrent.angle + totalA;
(_a = this._onUpdate) === null || _a === void 0 ? void 0 : _a.call(this, { x, y, angle });
// Stop
const linearStep = Math.hypot(dx, dy);
const angularStep = Math.abs(dAngle);
let shouldStop = linearStep < BELOW_THRESHOLD && angularStep < BELOW_THRESHOLD;
if (distance) {
shouldStop =
Math.abs(totalX - distance.x) < LERP_APPROX &&
Math.abs(totalY - distance.y) < LERP_APPROX &&
Math.abs(totalA - distance.angle) < LERP_APPROX;
}
if (!isBouncing && shouldStop) {
this.ctx.onEnd();
this._clear();
}
}
/** Calculate velocity progress */
_getVelocityProgress(v, initial) {
if (Math.abs(initial) < BELOW_THRESHOLD) {
return 1;
}
const p = 1 - Math.abs(v / initial);
if (Math.abs(1 - p) < LERP_APPROX / 10) {
return 1;
}
return p;
}
_predictDistance(velocity, decay, frameMs = 1000 / 60) {
const k = (decay * 60) / 1000;
const r = Math.exp(-k * frameMs);
return (velocity * frameMs) / (1 - r);
}
/** Apply exponential axis bounce overflow */
_applyAxisBounce(axis, value, velocity, bounds, ease) {
if (!bounds.length) {
return { value, velocity };
}
const snappy = this.ctx.coords.snap[axis];
const lo = typeof snappy === 'number' ? snappy : Math.min(...bounds);
const hi = typeof snappy === 'number' ? snappy : Math.max(...bounds);
if (value < lo || value > hi) {
const target = clamp(value, lo, hi);
const val = lerp(value, target, ease, LERP_APPROX);
const vel = lerp(velocity, 0, ease, LERP_APPROX);
return {
value: val,
velocity: vel,
bounceFinished: val === target && vel === 0,
};
}
return { value, velocity };
}
/** Clear data and stop animation */
_clear() {
var _a;
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.destroy();
this._raf = undefined;
this._velocity = Object.assign({}, IDLE_STATE);
}
/** Stop inertia animation */
cancel() {
if (this._raf) {
this.ctx.onCancel();
}
this._clear();
}
/** Destroy instance */
destroy() {
this._clear();
}
}
//# sourceMappingURL=index.js.map