vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
355 lines • 12.4 kB
JavaScript
import { initVevet } from '../../../global/initVevet';
import { isFiniteNumber } from '../../../internal/isFiniteNumber';
import { unwrapAngleDelta } from '../../../internal/unwrapAngle';
import { closest } from '../../../utils';
const START_VEC3 = { x: 0, y: 0, angle: 0 };
const START_STATE = Object.assign(Object.assign({}, START_VEC3), { time: 0 });
export class SwipeCoords {
constructor(ctx) {
this.ctx = ctx;
/** Event timestamp. */
this._timestamp = 0;
/** Start position. */
this._start = Object.assign({}, START_STATE);
/** Previous position. */
this._prev = Object.assign({}, START_STATE);
/** Current position. */
this._current = Object.assign({}, START_STATE);
/** Movement offset from start. */
this._diff = Object.assign({}, START_STATE);
/** Movement offset from previous position. */
this._step = Object.assign({}, START_STATE);
/** Total accumulated movement since swipe start. */
this._accum = Object.assign({}, START_VEC3);
/** Movement with rubber and snap applied (movement space). */
this._movement = Object.assign({}, START_VEC3);
/** Previous movement with rubber and snap applied (movement space). */
this._prevMovement = Object.assign({}, START_VEC3);
/** Raw accumulated displacement (before rubber). */
this._rawMovement = Object.assign({}, START_VEC3);
/** Raw atan2 angle and unwrapped cumulative angle. */
this._tempAngle = { raw: 0, unwrapped: 0 };
/** Active snap target per axis, if any. */
this._snap = {};
/** Cached normalized bounds (refreshed on swipe start). */
this._bounds = null;
/** Current scale modifier. */
this._scale = 1;
}
get timestamp() {
return this._timestamp;
}
get start() {
return this._start;
}
get prev() {
return this._prev;
}
get current() {
return this._current;
}
get diff() {
return this._diff;
}
get step() {
return this._step;
}
get accum() {
return this._accum;
}
/** Displacement in movement space (rubber + snap). */
get movement() {
return this._movement;
}
set movement(value) {
const newValue = Object.assign(Object.assign({}, this.movement), value);
this._movement.x = newValue.x;
this._movement.y = newValue.y;
this._movement.angle = newValue.angle;
this._rawMovement.x = newValue.x;
this._rawMovement.y = newValue.y;
this._rawMovement.angle = newValue.angle;
}
/** Previous displacement in movement space (rubber + snap). */
get prevMovement() {
return this._prevMovement;
}
/** Raw movement before rubber (same space as `bounds`). */
get rawMovement() {
return this._rawMovement;
}
/** Normalized movement limits (`[min, max]` per defined axis). */
get bounds() {
if (this._bounds) {
return this._bounds;
}
return this.calculateBounds();
}
get overflow() {
return this.ctx.props.overflow ? Math.abs(this.ctx.props.overflow()) : 0;
}
/** Current scale modifier */
get scale() {
return this._scale;
}
get coords() {
const { timestamp, start, prev, current, diff, step, accum, movement, prevMovement, scale, } = this;
return {
timestamp,
start,
prev,
current,
diff,
step,
accum,
movement,
prevMovement,
scale,
};
}
/** Resolved snap target per axis during the current gesture. */
get snap() {
return this._snap;
}
/**
* Overflow past `bounds` per axis in movement space.
* Zero when inside limits; used for bounce-back.
*/
get exceeds() {
const { _rawMovement: movement, bounds } = this;
if (!bounds) {
return null;
}
let xDiff = 0;
let yDiff = 0;
let aDiff = 0;
if (bounds.x) {
if (movement.x < bounds.x[0]) {
xDiff = movement.x - bounds.x[0];
}
else if (movement.x > bounds.x[1]) {
xDiff = movement.x - bounds.x[1];
}
}
if (bounds.y) {
if (movement.y < bounds.y[0]) {
yDiff = movement.y - bounds.y[0];
}
else if (movement.y > bounds.y[1]) {
yDiff = movement.y - bounds.y[1];
}
}
if (bounds.angle) {
if (movement.angle < bounds.angle[0]) {
aDiff = movement.angle - bounds.angle[0];
}
else if (movement.angle > bounds.angle[1]) {
aDiff = movement.angle - bounds.angle[1];
}
}
return {
x: xDiff,
y: yDiff,
angle: aDiff,
};
}
/** Parses pointer coordinates relative to the container */
decode(event) {
const vevet = initVevet();
const { props, container } = this.ctx;
let clientX = 0;
let clientY = 0;
if ('touches' in event) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
}
else if ('type' in event) {
clientX = event.clientX;
clientY = event.clientY;
}
else {
clientX = event.x;
clientY = event.y;
}
let x = clientX;
let y = clientY;
let centerX = vevet.width / 2;
let centerY = vevet.height / 2;
if (props.relative) {
const bounding = container.getBoundingClientRect();
x = clientX - bounding.left;
y = clientY - bounding.top;
centerX = bounding.left + bounding.width / 2;
centerY = bounding.top + bounding.height / 2;
}
const angleRad = Math.atan2(clientY - centerY, clientX - centerX);
const angle = (angleRad * 180) / Math.PI;
return {
x: x,
y: y,
angle,
time: performance.now(),
};
}
/** Apply scale and optionally zoom toward an origin in movement space. */
applyScale(value, originProp) {
if (this._scale === value) {
return;
}
if (originProp) {
const origin = this.decode(originProp);
const ratio = value / this._scale;
this.movement = {
x: origin.x - (origin.x - this._movement.x) * ratio,
y: origin.y - (origin.y - this._movement.y) * ratio,
};
}
this._scale = value;
}
/** Set start coordinates */
setStart(state) {
this._tempAngle = { raw: state.angle, unwrapped: state.angle };
this._timestamp = performance.now();
this._start = Object.assign({}, state);
this._prev = Object.assign({}, state);
this._current = Object.assign({}, state);
this._diff = Object.assign(Object.assign({}, START_VEC3), { time: 0 });
this._step = Object.assign(Object.assign({}, START_VEC3), { time: 0 });
this._accum = Object.assign({}, START_VEC3);
}
/** Sync temp angle */
syncTempAngle() {
this._tempAngle.raw = this._current.angle;
this._tempAngle.unwrapped = this._current.angle;
}
/** Update coordinates */
update({ x, y, angle, time }, applyRatio = true) {
// Vars
const { start, ctx } = this;
const stepRatio = applyRatio ? ctx.props.ratio : 1;
// Update bounds
if ((ctx.hasInertia() && ctx.recalculateBoundsOnInertia()) ||
!ctx.hasInertia()) {
this.calculateBounds();
}
// Save
this._timestamp = performance.now();
this._prev = Object.assign({}, this.current);
this._current = { x, y, angle, time };
const { _current: current, _prev: prev, overflow } = this;
// Update angle
this._updateTempAngle(angle);
current.angle = this._tempAngle.unwrapped;
// Update coords
this._step = {
x: current.x - prev.x,
y: current.y - prev.y,
angle: current.angle - prev.angle,
time: current.time - prev.time,
};
this._diff = {
x: current.x - start.x,
y: current.y - start.y,
angle: this._diff.angle + this._step.angle,
time: current.time - start.time,
};
this._accum = {
x: this._accum.x + Math.abs(this._step.x),
y: this._accum.y + Math.abs(this._step.y),
angle: this._accum.angle + Math.abs(this._step.angle),
};
this._rawMovement = {
x: this._rawMovement.x + this._step.x * stepRatio,
y: this._rawMovement.y + this._step.y * stepRatio,
angle: this._rawMovement.angle + this._step.angle * stepRatio,
};
this._prevMovement.x = this._movement.x;
this._prevMovement.y = this._movement.y;
this._prevMovement.angle = this._movement.angle;
this._movement.x = this._applyRubber('x', overflow);
this._movement.y = this._applyRubber('y', overflow);
this._movement.angle = this._applyRubber('angle', overflow);
this._snapMovementAxis('x');
this._snapMovementAxis('y');
this._snapMovementAxis('angle');
}
/** Snap movement axis */
_snapMovementAxis(axis) {
var _a;
const { props, hasInertia } = this.ctx;
const snap = (_a = props.snap) === null || _a === void 0 ? void 0 : _a.call(props);
if (!snap) {
this._snap[axis] = undefined;
return;
}
const snaps = snap[axis];
if (!(snaps === null || snaps === void 0 ? void 0 : snaps.length)) {
this._snap[axis] = undefined;
return;
}
const value = this._movement[axis];
const target = closest(value, snaps);
const radius = props.snapRadius;
if (isFiniteNumber(radius) && Math.abs(target - value) > Math.abs(radius)) {
this._snap[axis] = undefined;
return;
}
this._snap[axis] = target;
if (!hasInertia()) {
this._movement[axis] = target;
}
}
/** Calculate bounds */
calculateBounds() {
const { props } = this.ctx;
if (!props.bounds) {
this._bounds = null;
return;
}
const bounds = props.bounds(this.coords);
const d = [-Infinity, Infinity];
const x = (bounds === null || bounds === void 0 ? void 0 : bounds.x)
? [Math.min(...bounds.x), Math.max(...bounds.x)]
: [...d];
const y = (bounds === null || bounds === void 0 ? void 0 : bounds.y)
? [Math.min(...bounds.y), Math.max(...bounds.y)]
: [...d];
const a = (bounds === null || bounds === void 0 ? void 0 : bounds.angle)
? [Math.min(...bounds.angle), Math.max(...bounds.angle)]
: [...d];
this._bounds = { x, y, angle: a };
return this._bounds;
}
/** Unwrap raw atan2 angle and accumulate into _angle */
_updateTempAngle(rawAngle) {
this._tempAngle.unwrapped += unwrapAngleDelta(rawAngle, this._tempAngle.raw);
this._tempAngle.raw = rawAngle;
}
/** Apply rubber-band past movement bounds. */
_applyRubber(axis, overflow) {
var _a;
const temp = this._rawMovement[axis];
const bounds = (_a = this.bounds) === null || _a === void 0 ? void 0 : _a[axis];
if (!bounds) {
return temp;
}
const [min, max] = bounds;
if (temp >= min && temp <= max) {
return temp;
}
if (temp < min) {
return min - this._rubberDistance(min - temp, overflow);
}
return max + this._rubberDistance(temp - max, overflow);
}
/**
* Overscroll → rubber displacement
*/
_rubberDistance(overscroll, limit) {
if (overscroll <= 0 || limit <= 0) {
return 0;
}
return (limit * overscroll) / (limit + overscroll);
}
}
//# sourceMappingURL=index.js.map