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.

233 lines 9.86 kB
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