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.

355 lines 12.4 kB
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