UNPKG

@whee/js-motion

Version:
285 lines (283 loc) 9.74 kB
const noop = () => { // no operation }; class Motion { /** * Motion 构造函数 * @param {Object} [options] - 配置项 * @param {string} [options.target=HTMLElement|string] - 绑定元素 * @param {string} [options.direction=xy] - 移动记录方向:x 只记录水平方向,y 只记录垂直方向,xy 水平垂直方向都记录 * @param {string} [options.mode=realtime] - 模式: * 'realtime' 实时模式,实时计算触摸情况 * 'frame' 帧模式 */ constructor(options = {}) { this.mainFinger = 0; this.trendData = []; this.trendLength = 4; this.prevData = {}; this.renderData = null; this.frameId = 0; this.rendering = false; this.accumulation = 6; this.tmThreshold = 50; // 惯性滚动时间差阈值,超过该值不触发惯性滚动,ios 比较灵敏,android 不灵敏 this.touchstartHandler = noop; this.touchmoveHandler = noop; this.touchendHandler = noop; this.el = options.target ? this.getEl(options.target) : null; this.mode = options.mode || 'realtime'; this.direction = options.direction || 'xy'; this.coordinate = options.coordinate || 'page'; this.initEvent(); } getEl(el) { return typeof el === 'string' ? document.querySelector(el) : el; } initEvent() { if (!this.el) return; this.el.addEventListener('touchstart', e => { this.touchstart(e); this.touchstartHandler(e); }, Motion.isSupportPassive ? { passive: false, capture: true } : /* istanbul ignore next */ false); this.el.addEventListener('touchmove', e => { e.preventDefault(); this.touchmove(e, s => { this.touchmoveHandler(s, e); }); }, Motion.isSupportPassive ? { passive: false, capture: true } : /* istanbul ignore next */ false); this.el.addEventListener('touchend', e => { this.touchend(e, s => { this.touchendHandler(s, e); }); }); } createData(event, prop = 'targetTouches') { const firstTouch = event[prop][0]; const secondTouch = event[prop][1]; const pX = (this.coordinate + 'X'); const pY = (this.coordinate + 'Y'); const data = { x: firstTouch[pX], y: firstTouch[pY], t: Date.now(), l: 0, a: 0 }; if (secondTouch) { data.l = Math.sqrt(Math.pow(firstTouch[pX] - secondTouch[pY], 2) + Math.pow(firstTouch[pX] - secondTouch[pY], 2)); data.a = (Math.atan((secondTouch[pY] - firstTouch[pY]) / (secondTouch[pX] - firstTouch[pX])) * 180) / Math.PI; } // 收集数据 this.setTrendData(data); return data; } setTrendData(data) { if (this.trendData.length < 1) { this.trendData.push(data); return; } const lastData = this.trendData[this.trendData.length - 1]; const t = data.t - lastData.t; const x = data.x - lastData.x; const y = data.y - lastData.y; if (t > this.tmThreshold && Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / t < 0.3) { // 距上次数据时间差较大并且移动距离较小时(缓慢滚动),不收集滚动数据 this.trendData = []; } else { // 否则收集滚动数据,用于计算惯性滚动 this.trendData.push(data); } if (this.trendData.length > this.trendLength) this.trendData.shift(); } getMoveData(currData) { const prevData = this.prevData; const moveData = { x: currData.x - prevData.x, y: currData.y - prevData.y, t: currData.t - prevData.t, scale: currData.l === 0 || prevData.l === 0 ? 1 : currData.l / prevData.l, angle: prevData.a > 0 && currData.a < -80 ? currData.a - prevData.a + 180 : prevData.a < 0 && currData.a > 80 ? currData.a - prevData.a - 180 : currData.a - prevData.a }; this.prevData = currData; return moveData; } isNeedInertiaScroll() { return this.trendData.length > 1; } inertiaScroll(cb) { const first = this.trendData[0]; const last = this.trendData[this.trendData.length - 1]; const average = { x: last.x - first.x, y: last.y - first.y, t: last.t - first.t }; let xMoveStep = this.getMoveStep(average.x, average.t); let yMoveStep = this.getMoveStep(average.y, average.t); if (this.direction === 'x') { yMoveStep = () => 0; } else if (this.direction === 'y') { xMoveStep = () => 0; } const step = () => { const deltaSx = xMoveStep(); const deltaSy = yMoveStep(); if (deltaSx !== 0 || deltaSy !== 0) { this.frameId = requestAnimationFrame(step); } cb({ x: deltaSx, y: deltaSy, angle: 0, scale: 1 }); }; step(); } getMoveStep(s, t) { const v0 = s / t; const a0 = v0 / t / 10; let v = v0; let a = -a0 * 0.06; return /* step */ () => { const ratio = v / v0; const nextA = ratio > 0.4 ? a + 0.02 * a0 : a - 0.015 * a0; const nextV = v - a * this.accumulation; let deltaS = ((v + nextV) / 2) * this.accumulation; if (this.isMoveStop(v, nextV)) { // 停止运动 deltaS = 0; v = 0; a = 0; } else { v = nextV; a = nextA; } return deltaS; }; } /** * 速度为 0,或者速度方向改变,或者速度无限大,则停止运动 * @param v - 当前速度 * @param nextV - 下一刻速度 */ isMoveStop(v, nextV) { return v === Infinity || v === -Infinity || v === 0 || v / nextV < 0; } moveFrame(event, cb) { const data = this.createData(event); // 下一帧渲染 this.renderData = data; if (!this.rendering) { this.rendering = true; this.frameId = requestAnimationFrame(() => { const moveData = this.getMoveData(this.renderData); const cbData = { x: this.direction !== 'y' ? moveData.x : 0, y: this.direction !== 'x' ? moveData.y : 0, scale: moveData.scale, angle: moveData.angle }; cb(cbData); this.rendering = false; }); } } moveRealtime(event, cb) { const data = this.createData(event); const moveData = this.getMoveData(data); const cbData = { x: this.direction !== 'y' ? moveData.x : 0, y: this.direction !== 'x' ? moveData.y : 0, scale: moveData.scale, angle: moveData.angle }; cb(cbData); } onTouchstart(cb = noop) { this.touchstartHandler = cb; } onTouchmove(cb = noop) { this.touchmoveHandler = cb; } onTouchend(cb = noop) { this.touchendHandler = cb; } touchstart(event) { const targetTouches = event.targetTouches; const changedTouches = event.changedTouches; if (targetTouches.length === changedTouches.length) { // first touch, init touch const touch = event.changedTouches[0]; this.trendData = []; this.mainFinger = touch.identifier; this.clearInertiaScroll(); } // record touch data this.prevData = this.createData(event); } touchmove(event, cb = noop) { if (event.targetTouches[0].identifier === this.mainFinger) { // move with main finger this.mode === 'frame' ? this.moveFrame(event, cb) : this.moveRealtime(event, cb); } } touchend(event, cb = noop) { const targetTouches = event.targetTouches; if (targetTouches.length > 0) { // has other finger touch const firstTouch = targetTouches[0]; this.prevData = this.createData(event); if (this.mainFinger !== firstTouch.identifier) { // main finger leave, changed main finger this.trendData = []; this.mainFinger = firstTouch.identifier; } } else { // has no finger touch, emit touchend this.createData(event, 'changedTouches'); if (this.isNeedInertiaScroll()) { this.inertiaScroll(cb); } else { cb({ x: 0, y: 0, angle: 0, scale: 1 }); } } } clearInertiaScroll() { cancelAnimationFrame(this.frameId); } } Motion.isSupportPassive = (() => { let supportsPassive = false; try { document.createElement('div').addEventListener('testPassive', noop, { get passive() { supportsPassive = true; return false; } }); } catch (e) { // Not support } return supportsPassive; })(); export default Motion;