@whee/js-motion
Version:
motion for mobile
285 lines (283 loc) • 9.74 kB
JavaScript
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;