nippleJs是什么?
nippleJs是一个虚拟摇杆的js库,专为可触摸的设备提供接口,常被用于游戏和可操作硬件设备的app或网页中。
nippleJs使用
demo
官方文档或者我之前写的一篇简单的教程传送门
源码分析
项目目录结构
这里只看源码相关的文件。
src
├── collection.js // collection 类
├── index.js
├── manager.js // manager 类
├── nipple.js // nipple 类
├── super.js // super 类,是上面其它类对象的公共父类
└── utils.js // 提供了多个计算函数
分析顺序从最简单的开始
utils.js
///////////////////////
/// UTILS ///
///////////////////////
// 计算两点之间的直线距离,两条直角边求斜线长度
export const distance = (p1, p2) => {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt((dx * dx) + (dy * dy));
};
//1弧度=180/pai 度 1度=pai/180
//弧度 Math.atan2(y, x)返回-pai和pai 单位为弧度
//通过公式返回角度值
export const angle = (p1, p2) => {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return degrees(Math.atan2(dy, dx));
};
//得到坐标
//斜边c,求直角边b和高a:
//b=ccosα,a=csinα => 斜边c就是之前算出来的d 直角边b 对应x的长度 高a为y的高度
export const findCoord = (p, d, a) => {
const b = {x: 0, y: 0};
a = radians(a);
b.x = p.x - d * Math.cos(a);
b.y = p.y - d * Math.sin(a);
return b;
};
//转化弧度
export const radians = (a) => {
return a * (Math.PI / 180);
};
//转化角度
export const degrees = (a) => {
return a * (180 / Math.PI);
};
export const isPressed = (evt) => {
if (isNaN(evt.buttons)) {
return evt.pressure !== 0;
}
return evt.buttons !== 0;
};
// 绑定事件
export const bindEvt = (el, arg, handler) => {
//将字符串分割为数组,如 "a,c,b,e" => ["a","c","b","e"]
const types = arg.split(/[ ,]+/g);
let type;
// 通过参数来绑定元素上对应的事件以及回调函数
for (let i = 0; i < types.length; i += 1) {
type = types[i];
//ie 7 ie 8不支持 addEventListener,addEventListener的第3个参数代表事件类型是否是捕获事件,默认false为冒泡
if (el.addEventListener) {
el.addEventListener(type, handler, false);
} else if (el.attachEvent) {
el.attachEvent(type, handler);
}
}
};
// 解绑事件
export const unbindEvt = (el, arg, handler) => {
const types = arg.split(/[ ,]+/g);
let type;
for (let i = 0; i < types.length; i += 1) {
type = types[i];
if (el.removeEventListener) {
el.removeEventListener(type, handler);
} else if (el.detachEvent) {
el.detachEvent(type, handler);
}
}
};
// 触发元素的某一类型事件
export const trigger = (el, type, data) => {
const evt = new CustomEvent(type, data);
el.dispatchEvent(evt);
};
//阻止默认事件
//evt.changedTouches 返回touch对象的一个属性 TouchList
//这个 TouchList 对象列出了和这个触摸事件对应的 Touch 对象。
//对于 touchstart 事件, 这个 TouchList 对象列出在此次事件中新增加的触点。
//对于 touchmove 事件,列出和上一次事件相比较,发生了变化的触点。
//对于touchend事件,changedTouches 是已经从触摸面的离开的触点的集合(也就是说,手指已经离开了屏幕/触摸面)。
export const prepareEvent = (evt) => {
evt.preventDefault();
return evt.type.match(/^touch/) ? evt.changedTouches : evt;
};
//返回滚动条 x轴 y轴的滚动距离
export const getScroll = () => {
const x = (window.pageXOffset !== undefined) ?
window.pageXOffset :
(document.documentElement || document.body.parentNode || document.body)
.scrollLeft;
const y = (window.pageYOffset !== undefined) ?
window.pageYOffset :
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
return {
x: x,
y: y
};
};
// 设置元素的位置,根据传入的参数不同会有两种情况
export const applyPosition = (el, pos) => {
if (pos.top || pos.right || pos.bottom || pos.left) {
el.style.top = pos.top;
el.style.right = pos.right;
el.style.bottom = pos.bottom;
el.style.left = pos.left;
} else {
el.style.left = pos.x + 'px';
el.style.top = pos.y + 'px';
}
};
//{key: "", webkitKey: "", MozKey: "", oKey: ""}对这样的对象进行赋值,做样式的兼容性处理
//这里会加上一个时间结尾的值,值类型应该是str 和 [str]两种
export const getTransitionStyle = (property, values, time) => {
const obj = configStylePropertyObject(property);
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
if (typeof values === 'string') {
obj[i] = values + ' ' + time;
} else {
let st = '';
for (let j = 0, max = values.length; j < max; j += 1) {
st += values[j] + ' ' + time + ', ';
}
obj[i] = st.slice(0, -2);
}
}
}
return obj;
};
//{key: "", webkitKey: "", MozKey: "", oKey: ""}对这样的对象进行赋值,做样式的兼容性处理
export const getVendorStyle = (property, value) => {
const obj = configStylePropertyObject(property);
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
obj[i] = value;
}
}
return obj;
};
// configStylePropertyObject('key')
// return {key: "", webkitKey: "", MozKey: "", oKey: ""}
export const configStylePropertyObject = (prop) => {
const obj = {};
obj[prop] = '';
const vendors = ['webkit', 'Moz', 'o'];
vendors.forEach(function (vendor) {
obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = '';
});
return obj;
};
//objA新增objB的属性 会改变objA原有属性,并返回该对象
export const extend = (objA, objB) => {
for (let i in objB) {
if (objB.hasOwnProperty(i)) {
objA[i] = objB[i];
}
}
return objA;
};
// 不改变objA,返回一个新对象并且当AB有共同key的时候 会保留B的值
// Overwrite only whats already present
export const safeExtend = (objA, objB) => {
const obj = {};
for (let i in objA) {
if (objA.hasOwnProperty(i) && objB.hasOwnProperty(i)) {
obj[i] = objB[i];
} else if (objA.hasOwnProperty(i)) {
obj[i] = objA[i];
}
}
return obj;
};
// Map for array or unique item.
//遍历ar把所有的单项作为参数执行一遍
export const map = (ar, fn) => {
if (ar.length) {
for (let i = 0, max = ar.length; i < max; i += 1) {
fn(ar[i]);
}
} else {
fn(ar);
}
};
super.js
///////////////////////
/// SUPER CLASS ///
///////////////////////
import * as u from './utils';
// 设置常量
// Constants
// 判断是否是有touch 对象 pointer 对象
var isTouch = !!('ontouchstart' in window);
// PointerEvent 接口代表了由 指针 引发的DOM事件的状态,包括接触点的位置,引发事件的设备类型,接触表面受到的压力等。
// 指针 是输入设备的硬件层抽象(比如鼠标,触摸笔,或触摸屏上的一个触摸点)。指针 能指向一个具体表面(如屏幕)上的一个(或一组)坐标。
// 指针的 击中检测 指浏览器用来检测 指针事件的目标元素的过程。大多数情况下,这个目标元素是由 指针的位置和元素在文章中的位置和分层共同决定的
var isPointer = window.PointerEvent ? true : false;
var isMSPointer = window.MSPointerEvent ? true : false;
var events = {
touch: {
start: 'touchstart',
move: 'touchmove',
end: 'touchend, touchcancel'
},
mouse: {
start: 'mousedown',
move: 'mousemove',
end: 'mouseup'
},
pointer: {
start: 'pointerdown',
move: 'pointermove',
end: 'pointerup, pointercancel'
},
MSPointer: {
start: 'MSPointerDown',
move: 'MSPointerMove',
end: 'MSPointerUp'
}
};
var toBind;
var secondBind = {};
if (isPointer) {
toBind = events.pointer;
} else if (isMSPointer) {
toBind = events.MSPointer;
} else if (isTouch) {
toBind = events.touch;
secondBind = events.mouse;
} else {
toBind = events.mouse;
}
// 这个库 所有类的继承对象
function Super () {}
// Basic event system.
// 自定义事件,发布订阅模式 通过‘a,b,c’去监听 a b c 事件 并储存对应的回调函数,通过trigger触发
Super.prototype.on = function (arg, cb) {
var self = this;
var types = arg.split(/[ ,]+/g);
var type;
self._handlers_ = self._handlers_ || {};
for (var i = 0; i < types.length; i += 1) {
type = types[i];
//用数组去维护同种类型事件的 回调函数
self._handlers_[type] = self._handlers_[type] || [];
self._handlers_[type].push(cb);
}
return self;
};
Super.prototype.off = function (type, cb) {
var self = this;
self._handlers_ = self._handlers_ || {};
if (type === undefined) {
self._handlers_ = {};
} else if (cb === undefined) {
self._handlers_[type] = null;
} else if (self._handlers_[type] &&
self._handlers_[type].indexOf(cb) >= 0) {
self._handlers_[type].splice(self._handlers_[type].indexOf(cb), 1);
}
return self;
};
Super.prototype.trigger = function (arg, data) {
var self = this;
var types = arg.split(/[ ,]+/g);
var type;
self._handlers_ = self._handlers_ || {};
for (var i = 0; i < types.length; i += 1) {
type = types[i];
if (self._handlers_[type] && self._handlers_[type].length) {
self._handlers_[type].forEach(function (handler) {
handler.call(self, {
type: type,
target: self
}, data);
});
}
}
};
// Configuration
// 配置属性,通过返回一个新对象 浅拷贝
Super.prototype.config = function (options) {
var self = this;
self.options = self.defaults || {};
if (options) {
self.options = u.safeExtend(self.options, options);
}
};
// Bind internal events.
// 绑定内部原生事件,如果不存在该事件就会报错
Super.prototype.bindEvt = function (el, type) {
var self = this;
self._domHandlers_ = self._domHandlers_ || {};
self._domHandlers_[type] = function () {
if (typeof self['on' + type] === 'function') {
self['on' + type].apply(self, arguments);
} else {
// eslint-disable-next-line no-console
console.warn('[WARNING] : Missing "on' + type + '" handler.');
}
};
u.bindEvt(el, toBind[type], self._domHandlers_[type]);
if (secondBind[type]) {
// Support for both touch and mouse at the same time.
u.bindEvt(el, secondBind[type], self._domHandlers_[type]);
}
return self;
};
// Unbind dom events.
Super.prototype.unbindEvt = function (el, type) {
var self = this;
self._domHandlers_ = self._domHandlers_ || {};
u.unbindEvt(el, toBind[type], self._domHandlers_[type]);
if (secondBind[type]) {
// Support for both touch and mouse at the same time.
u.unbindEvt(el, secondBind[type], self._domHandlers_[type]);
}
delete self._domHandlers_[type];
return this;
};
export default Super;
在这里首先是判断运行环境中触摸元素的事件对象,做了一个适配层。目前的主流是window.PointerEvent(什么你没听过?没关系,这里有介绍和整合鼠标、触摸 和触控笔事件的Pointer Event Api)。简单来说就是比原有的touch 和 mouse事件对象更加强大,继承了mouseEvent对象并多了一些可读的属性 如对屏幕按压的强度,触碰点的宽度等等。
其它类都继承了super 类,super 类实现了一个基本的事件系统。其中有两个类型分别是自定义事件和原生事件。所以我们在文档上会发现manager、nipple对象里有很多属性是一样的,知道这点就不会啥啥分不清它们之间的区别了。
nipple.js(nipple类,生成摇杆实例)
import Super from './super';
import * as u from './utils';
///////////////////////
/// THE NIPPLE ///
///////////////////////
function Nipple (collection, options) {
this.identifier = options.identifier;
this.position = options.position;
this.frontPosition = options.frontPosition;
this.collection = collection;
// Defaults
this.defaults = {
size: 100,
threshold: 0.1,
color: 'white',
fadeTime: 250,
dataOnly: false,
restJoystick: true,
restOpacity: 0.5,
mode: 'dynamic',
zone: document.body,
lockX: false,
lockY: false
};
this.config(options);
// Overwrites
if (this.options.mode === 'dynamic') {
this.options.restOpacity = 0;
}
this.id = Nipple.id;
Nipple.id += 1;
this.buildEl()
.stylize();
// Nipple s API.
// 把原型上的属性值绑定到实例对象中
this.instance = {
el: this.ui.el,
on: this.on.bind(this),
off: this.off.bind(this),
show: this.show.bind(this),
hide: this.hide.bind(this),
add: this.addToDom.bind(this),
remove: this.removeFromDom.bind(this),
destroy: this.destroy.bind(this),
setPosition:this.setPosition.bind(this),
resetDirection: this.resetDirection.bind(this),
computeDirection: this.computeDirection.bind(this),
trigger: this.trigger.bind(this),
position: this.position,
frontPosition: this.frontPosition,
ui: this.ui,
identifier: this.identifier,
id: this.id,
options: this.options
};
return this.instance;
}
//继承 Super 属性 ,组合继承
Nipple.prototype = new Super();
Nipple.constructor = Nipple;
Nipple.id = 0;
// Build the dom element of the Nipple instance.
Nipple.prototype.buildEl = function (options) {
this.ui = {};
if (this.options.dataOnly) {
return this;
}
//创建三个div 分别对应 摇杆的三个部分 背景 前景
this.ui.el = document.createElement('div');
this.ui.back = document.createElement('div');
this.ui.front = document.createElement('div');
this.ui.el.className = 'nipple collection_' + this.collection.id;
this.ui.back.className = 'back';
this.ui.front.className = 'front';
this.ui.el.setAttribute('id', 'nipple_' + this.collection.id +
'_' + this.id);
this.ui.el.appendChild(this.ui.back);
this.ui.el.appendChild(this.ui.front);
return this;
};
// Apply CSS to the Nipple instance.
// 设置当前三个元素 back front el的样式
Nipple.prototype.stylize = function () {
if (this.options.dataOnly) {
return this;
}
var animTime = this.options.fadeTime + 'ms';
//borderStyle transitStyle 做了浏览器兼容 其它就写死
//key: "", webkitKey: "", MozKey: "", oKey: ""
var borderStyle = u.getVendorStyle('borderRadius', '50%');
var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime);
var styles = {};
styles.el = {
position: 'absolute',
opacity: this.options.restOpacity,
display: 'block',
'zIndex': 999
};
styles.back = {
position: 'absolute',
display: 'block',
width: this.options.size + 'px',
height: this.options.size + 'px',
marginLeft: -this.options.size / 2 + 'px',
marginTop: -this.options.size / 2 + 'px',
background: this.options.color,
'opacity': '.5'
};
styles.front = {
width: this.options.size / 2 + 'px',
height: this.options.size / 2 + 'px',
position: 'absolute',
display: 'block',
marginLeft: -this.options.size / 4 + 'px',
marginTop: -this.options.size / 4 + 'px',
background: this.options.color,
'opacity': '.5'
};
//在原有的对象上新增 需要兼容的style属性
u.extend(styles.el, transitStyle);
u.extend(styles.back, borderStyle);
u.extend(styles.front, borderStyle);
// 运行函数 设置元素的style的样式
this.applyStyles(styles);
return this;
};
Nipple.prototype.applyStyles = function (styles) {
// Apply styles
for (var i in this.ui) {
if (this.ui.hasOwnProperty(i)) {
for (var j in styles[i]) {
this.ui[i].style[j] = styles[i][j];
}
}
}
return this;
};
// Inject the Nipple instance into DOM.
Nipple.prototype.addToDom = function () {
// We're not adding it if we're dataOnly or already in dom.
if (this.options.dataOnly || document.body.contains(this.ui.el)) {
return this;
}
this.options.zone.appendChild(this.ui.el);
return this;
};
// Remove the Nipple instance from DOM.
Nipple.prototype.removeFromDom = function () {
if (this.options.dataOnly || !document.body.contains(this.ui.el)) {
return this;
}
this.options.zone.removeChild(this.ui.el);
return this;
};
// Entirely destroy this nipple
Nipple.prototype.destroy = function () {
clearTimeout(this.removeTimeout);
clearTimeout(this.showTimeout);
clearTimeout(this.restTimeout);
this.trigger('destroyed', this.instance);
this.removeFromDom();
this.off();
};
// Fade in the Nipple instance.
Nipple.prototype.show = function (cb) {
var self = this;
if (self.options.dataOnly) {
return self;
}
clearTimeout(self.removeTimeout);
clearTimeout(self.showTimeout);
clearTimeout(self.restTimeout);
self.addToDom();
self.restCallback();
setTimeout(function () {
self.ui.el.style.opacity = 1;
}, 0);
self.showTimeout = setTimeout(function () {
self.trigger('shown', self.instance);
if (typeof cb === 'function') {
cb.call(this);
}
}, self.options.fadeTime);
return self;
};
// Fade out the Nipple instance.
Nipple.prototype.hide = function (cb) {
var self = this;
if (self.options.dataOnly) {
return self;
}
self.ui.el.style.opacity = self.options.restOpacity;
clearTimeout(self.removeTimeout);
clearTimeout(self.showTimeout);
clearTimeout(self.restTimeout);
self.removeTimeout = setTimeout(
function () {
var display = self.options.mode === 'dynamic' ? 'none' : 'block';
self.ui.el.style.display = display;
if (typeof cb === 'function') {
cb.call(self);
}
self.trigger('hidden', self.instance);
},
self.options.fadeTime
);
if (self.options.restJoystick) {
self.setPosition(cb, { x: 0, y: 0 });
}
return self;
};
// Set the nipple to the specified position
Nipple.prototype.setPosition = function (cb, position) {
var self = this;
self.frontPosition = {
x: position.x,
y: position.y
};
var animTime = self.options.fadeTime + 'ms';
var transitStyle = {};
transitStyle.front = u.getTransitionStyle('transition',
['top', 'left'], animTime);
var styles = {front: {}};
styles.front = {
left: self.frontPosition.x + 'px',
top: self.frontPosition.y + 'px'
};
self.applyStyles(transitStyle);
self.applyStyles(styles);
self.restTimeout = setTimeout(
function () {
if (typeof cb === 'function') {
cb.call(self);
}
self.restCallback();
},
self.options.fadeTime
);
};
Nipple.prototype.restCallback = function () {
var self = this;
var transitStyle = {};
transitStyle.front = u.getTransitionStyle('transition', 'none', '');
self.applyStyles(transitStyle);
self.trigger('rested', self.instance);
};
Nipple.prototype.resetDirection = function () {
// Fully rebuild the object to let the iteration possible.
this.direction = {
x: false,
y: false,
angle: false
};
};
Nipple.prototype.computeDirection = function (obj) {
var rAngle = obj.angle.radian;
//1弧度=180/pai 度 1度=pai/180
var angle45 = Math.PI / 4;
var angle90 = Math.PI / 2;
var direction, directionX, directionY;
//角度偏移
// Angular direction
// \ UP /
// \ /
// LEFT RIGHT
// / \
// /DOWN \
//
if (
rAngle > angle45 &&
rAngle < (angle45 * 3) &&
!obj.lockX
) {
direction = 'up';
} else if (
rAngle > -angle45 &&
rAngle <= angle45 &&
!obj.lockY
) {
direction = 'left';
} else if (
rAngle > (-angle45 * 3) &&
rAngle <= -angle45 &&
!obj.lockX
) {
direction = 'down';
} else if (!obj.lockY) {
direction = 'right';
}
// Plain direction
// UP |
// _______ | RIGHT
// LEFT |
// DOWN |
if (!obj.lockY) {
if (rAngle > -angle90 && rAngle < angle90) {
directionX = 'left';
} else {
directionX = 'right';
}
}
if (!obj.lockX) {
if (rAngle > 0) {
directionY = 'up';
} else {
directionY = 'down';
}
}
if (obj.force > this.options.threshold) {
var oldDirection = {};
var i;
for (i in this.direction) {
if (this.direction.hasOwnProperty(i)) {
oldDirection[i] = this.direction[i];
}
}
var same = {};
this.direction = {
x: directionX,
y: directionY,
angle: direction
};
obj.direction = this.direction;
for (i in oldDirection) {
if (oldDirection[i] === this.direction[i]) {
same[i] = true;
}
}
// If all 3 directions are the same, we don t trigger anything.
if (same.x && same.y && same.angle) {
return obj;
}
if (!same.x || !same.y) {
this.trigger('plain', obj);
}
if (!same.x) {
this.trigger('plain:' + directionX, obj);
}
if (!same.y) {
this.trigger('plain:' + directionY, obj);
}
if (!same.angle) {
this.trigger('dir dir:' + direction, obj);
}
} else {
this.resetDirection();
}
return obj;
};
export default Nipple;
nipple对象可生成三个el元素并设置了初始样式,对部分css3样式还做了兼容性处理。其中元素分别为container, front 和 back,通过对应的class名可以自定义自己想要的摇杆样式。mode也有三种,完全动态的,半动态的以及静态的,不同类型表现方式会略有差别,还通过utils里提供的方法可以算出当前摇杆所处的方向等等。
collection.js
import Nipple from './nipple';
import Super from './super';
import * as u from './utils';
///////////////////////////
/// THE COLLECTION ///
///////////////////////////
//收集器对象 用来存储并操作nipple对象
function Collection (manager, options) {
var self = this;
self.nipples = [];
self.idles = [];//?
self.actives = [];//?
self.ids = [];//?
self.pressureIntervals = {};//?
self.manager = manager; // 操作区域的dom
self.id = Collection.id; // 对象 id
Collection.id += 1;
// Defaults
self.defaults = {
zone: document.body,
multitouch: false,
maxNumberOfNipples: 10,
mode: 'dynamic',
position: {top: 0, left: 0},
catchDistance: 200,
size: 100,
threshold: 0.1,
color: 'white',
fadeTime: 250,
dataOnly: false,
restJoystick: true,
restOpacity: 0.5,
lockX: false,
lockY: false,
dynamicPage: false
};
//通过传入的对象拷贝其属性来拓展自身的属性
self.config(options);
// Overwrites
// 如果模式是半自动或者禁止状态那么就关闭多点触摸
if (self.options.mode === 'static' || self.options.mode === 'semi') {
self.options.multitouch = false;
}
// 如果多点触控被关闭那么nipple的最大数量为1
if (!self.options.multitouch) {
self.options.maxNumberOfNipples = 1;
}
//self.box = self.options.zone.getBoundingClientRect();
//重新获取box的元素属性
self.updateBox();
//对自身nipples数组进行初始化操作,把一些公共的方法也设置上去 继承
self.prepareNipples();
self.bindings();
self.begin();
return self.nipples;
}
Collection.prototype = new Super();
Collection.constructor = Collection;
Collection.id = 0;
Collection.prototype.prepareNipples = function () {
var self = this;
var nips = self.nipples;
// Public API Preparation.
// 对数组对象进行赋值,在里面绑定公共方法,不会影响数组本身的长度
nips.on = self.on.bind(self);
nips.off = self.off.bind(self);
nips.options = self.options;
nips.destroy = self.destroy.bind(self);
nips.ids = self.ids;
nips.id = self.id;
nips.processOnMove = self.processOnMove.bind(self);
nips.processOnEnd = self.processOnEnd.bind(self);
nips.get = function (id) {
if (id === undefined) {
return nips[0];
}
for (var i = 0, max = nips.length; i < max; i += 1) {
if (nips[i].identifier === id) {
return nips[i];
}
}
return false;
};
};
Collection.prototype.bindings = function () {
var self = this;
// Touch start event.
self.bindEvt(self.options.zone, 'start');
// Avoid native touch actions (scroll, zoom etc...) on the zone.
// 禁用浏览器默认的touch事件 所带来的行为
self.options.zone.style.touchAction = 'none';
self.options.zone.style.msTouchAction = 'none';
};
Collection.prototype.begin = function () {
var self = this;
var opts = self.options;
// We place our static nipple
// if needed.
// 如果是静态模式下的默认新增一个nipple对象 并插入到dom中
if (opts.mode === 'static') {
var nipple = self.createNipple(
opts.position,
self.manager.getIdentifier()
);
// Add it to the dom.
nipple.add();
// Store it in idles.
self.idles.push(nipple);
}
};
// Nipple Factory
Collection.prototype.createNipple = function (position, identifier) {
var self = this;
var scroll = u.getScroll();
var toPutOn = {};
var opts = self.options;
if (position.x && position.y) {
toPutOn = {
x: position.x -
(scroll.x + self.box.left),
y: position.y -
(scroll.y + self.box.top)
};
} else if (
position.top ||
position.right ||
position.bottom ||
position.left
) {
// We need to compute the position X / Y of the joystick.
var dumb = document.createElement('DIV');
dumb.style.display = 'hidden';
dumb.style.top = position.top;
dumb.style.right = position.right;
dumb.style.bottom = position.bottom;
dumb.style.left = position.left;
dumb.style.position = 'absolute';
opts.zone.appendChild(dumb);
var dumbBox = dumb.getBoundingClientRect();
opts.zone.removeChild(dumb);
toPutOn = position;
position = {
x: dumbBox.left + scroll.x,
y: dumbBox.top + scroll.y
};
}
var nipple = new Nipple(self, {
color: opts.color,
size: opts.size,
threshold: opts.threshold,
fadeTime: opts.fadeTime,
dataOnly: opts.dataOnly,
restJoystick: opts.restJoystick,
restOpacity: opts.restOpacity,
mode: opts.mode,
identifier: identifier,
position: position,
zone: opts.zone,
frontPosition: {
x: 0,
y: 0
}
});
if (!opts.dataOnly) {
u.applyPosition(nipple.ui.el, toPutOn);
u.applyPosition(nipple.ui.front, nipple.frontPosition);
}
self.nipples.push(nipple);
self.trigger('added ' + nipple.identifier + ':added', nipple);
self.manager.trigger('added ' + nipple.identifier + ':added', nipple);
self.bindNipple(nipple);
return nipple;
};
Collection.prototype.updateBox = function () {
var self = this;
self.box = self.options.zone.getBoundingClientRect();
};
Collection.prototype.bindNipple = function (nipple) {
var self = this;
var type;
// Bubble up identified events.
var handler = function (evt, data) {
// Identify the event type with the nipples id.
type = evt.type + ' ' + data.id + ':' + evt.type;
self.trigger(type, data);
};
// When it gets destroyed.
nipple.on('destroyed', self.onDestroyed.bind(self));
// Other events that will get bubbled up.
// 给nipple对象绑定多个自定义事件,显示 隐藏 上 下 左 右
nipple.on('shown hidden rested dir plain', handler);
nipple.on('dir:up dir:right dir:down dir:left', handler);
nipple.on('plain:up plain:right plain:down plain:left', handler);
};
Collection.prototype.pressureFn = function (touch, nipple, identifier) {
var self = this;
var previousPressure = 0;
clearInterval(self.pressureIntervals[identifier]);
// Create an interval that will read the pressure every 100ms
self.pressureIntervals[identifier] = setInterval(function () {
var pressure = touch.force || touch.pressure ||
touch.webkitForce || 0;
if (pressure !== previousPressure) {
nipple.trigger('pressure', pressure);
self.trigger('pressure ' +
nipple.identifier + ':pressure', pressure);
previousPressure = pressure;
}
}.bind(self), 100);
};
Collection.prototype.onstart = function (evt) {
var self = this;
var opts = self.options;
var origEvt = evt;
// 判断事件是否为touch事件,如果是touch事件那么返回changedTouches: 涉及当前事件的触摸点(手指)的列表,否则还是当前的evt
evt = u.prepareEvent(evt);
// Update the box position
self.updateBox();
var process = function (touch) {
// If we can create new nipples
// meaning we dont have more active nipples than we should.
if (self.actives.length < opts.maxNumberOfNipples) {
self.processOnStart(touch);
}
else if(origEvt.type.match(/^touch/)){
// zombies occur when end event is not received on Safari
// first touch removed before second touch, we need to catch up...
// so remove where touches in manager that no longer exist
Object.keys(self.manager.ids).forEach(function(k){
if(Object.values(origEvt.touches).findIndex(function(t){return t.identifier===k;}) < 0){
// manager has id that doesnt exist in touches
var e = [evt[0]];
e.identifier = k;
self.processOnEnd(e);
}
});
if(self.actives.length < opts.maxNumberOfNipples){
self.processOnStart(touch);
}
}
};
u.map(evt, process);
// We ask upstream to bind the document
// on 'move' and 'end'
self.manager.bindDocument();
return false;
};
Collection.prototype.processOnStart = function (evt) {
var self = this;
var opts = self.options;
var indexInIdles;
var identifier = self.manager.getIdentifier(evt);
var pressure = evt.force || evt.pressure || evt.webkitForce || 0;
var position = {
x: evt.pageX,
y: evt.pageY
};
var nipple = self.getOrCreate(identifier, position);
// Update its touch identifier
if (nipple.identifier !== identifier) {
self.manager.removeIdentifier(nipple.identifier);
}
nipple.identifier = identifier;
var process = function (nip) {
// Trigger the start.
nip.trigger('start', nip);
self.trigger('start ' + nip.id + ':start', nip);
nip.show();
if (pressure > 0) {
self.pressureFn(evt, nip, nip.identifier);
}
// Trigger the first move event.
self.processOnMove(evt);
};
// Transfer it from idles to actives.
if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) {
self.idles.splice(indexInIdles, 1);
}
// Store the nipple in the actives array
self.actives.push(nipple);
self.ids.push(nipple.identifier);
if (opts.mode !== 'semi') {
process(nipple);
} else {
// In semi we check the distance of the touch
// to decide if we have to reset the nipple
var distance = u.distance(position, nipple.position);
if (distance <= opts.catchDistance) {
process(nipple);
} else {
nipple.destroy();
self.processOnStart(evt);
return;
}
}
return nipple;
};
// collection 创建nipple方法
// 半自动和静态模式下 一般会存在一个静止的nipple对象
//如果self.idles[0]存在则返回该对象并在队列中移除
//如果semi里不存在nipple对象那么就创建一个并返回
Collection.prototype.getOrCreate = function (identifier, position) {
var self = this;
var opts = self.options;
var nipple;
// If we are in static or semi, we might already have an active.
if (/(semi|static)/.test(opts.mode)) {
// Get the active one.
// TODO: Multi-touche for semi and static will start here.
// Return the nearest one.
nipple = self.idles[0];
if (nipple) {
self.idles.splice(0, 1);
return nipple;
}
if (opts.mode === 'semi') {
// If we are in semi mode, we need to create one.
return self.createNipple(position, identifier);
}
// eslint-disable-next-line no-console
console.warn('Coudlnt find the needed nipple.');
return false;
}
// In dynamic, we create a new one.
nipple = self.createNipple(position, identifier);
return nipple;
};
//移动过程
// 获取事件id 通过事件id获取nipple对象 两者一致,计算出角度值 弧度值,当前的位置和nipple对象的距离,size应该是半径长度和压力值
// 如果lockx locky为true 那么就把摇杆的位置变为0 不改变其位置
// 通过nipple.computeDirection计算出摇杆的方向 并赋值给toSend
Collection.prototype.processOnMove = function (evt) {
var self = this;
var opts = self.options;
var identifier = self.manager.getIdentifier(evt);
var nipple = self.nipples.get(identifier);
// If we're moving without pressing
// it's that we went out the active zone
if (!u.isPressed(evt)) {
this.processOnEnd(evt);
return;
}
if (!nipple) {
// This is here just for safety.
// It shouldnt happen.
// eslint-disable-next-line no-console
console.error('Found zombie joystick with ID ' + identifier);
self.manager.removeIdentifier(identifier);
return;
}
if (opts.dynamicPage) {
var scroll = u.getScroll();
pos = nipple.el.getBoundingClientRect();
nipple.position = {
x: scroll.x + pos.left,
y: scroll.y + pos.top
};
}
nipple.identifier = identifier;
var size = nipple.options.size / 2;
var pos = {
x: evt.pageX,
y: evt.pageY
};
if (opts.lockX){
pos.y = nipple.position.y;
}
if (opts.lockY) {
pos.x = nipple.position.x;
}
var dist = u.distance(pos, nipple.position);
var angle = u.angle(pos, nipple.position);
var rAngle = u.radians(angle);
var force = dist / size;
var raw = {
distance: dist,
position: pos
};
// If distance is bigger than nipple s size
// we clamp the position.
if (dist > size) {
dist = size;
pos = u.findCoord(nipple.position, dist, angle);
}
var xPosition = pos.x - nipple.position.x;
var yPosition = pos.y - nipple.position.y;
nipple.frontPosition = {
x: xPosition,
y: yPosition
};
if (!opts.dataOnly) {
u.applyPosition(nipple.ui.front, nipple.frontPosition);
}
// Prepare event s datas.
var toSend = {
identifier: nipple.identifier,
position: pos,
force: force,
pressure: evt.force || evt.pressure || evt.webkitForce || 0,
distance: dist,
angle: {
radian: rAngle,
degree: angle
},
vector: {
x: xPosition / size,
y: - yPosition / size
},
raw: raw,
instance: nipple,
lockX: opts.lockX,
lockY: opts.lockY
};
// Compute the direction s datas.
toSend = nipple.computeDirection(toSend);
// Offset angles to follow units circle.
// 偏斜角跟随单位圆?算出互补角
toSend.angle = {
radian: u.radians(180 - angle),
degree: 180 - angle
};
// Send everything to everyone.
nipple.trigger('move', toSend);
self.trigger('move ' + nipple.id + ':move', toSend);
};
// 移动结束
// 如果是动态模式那么就移除当前nipple对象把所有类对象的remove触发一边
// 清除 self.pressureIntervals[nipple.identifier]
// nipple位置初始化
// 触发 nipple和 Collection的end事件
// 移除
Collection.prototype.processOnEnd = function (evt) {
var self = this;
var opts = self.options;
var identifier = self.manager.getIdentifier(evt);
var nipple = self.nipples.get(identifier);
var removedIdentifier = self.manager.removeIdentifier(nipple.identifier);
if (!nipple) {
return;
}
if (!opts.dataOnly) {
nipple.hide(function () {
if (opts.mode === 'dynamic') {
nipple.trigger('removed', nipple);
self.trigger('removed ' + nipple.id + ':removed', nipple);
self.manager
.trigger('removed ' + nipple.id + ':removed', nipple);
nipple.destroy();
}
});
}
// Clear the pressure interval reader
clearInterval(self.pressureIntervals[nipple.identifier]);
// Reset the direciton of the nipple, to be able to trigger a new direction
// on start.
nipple.resetDirection();
nipple.trigger('end', nipple);
self.trigger('end ' + nipple.id + ':end', nipple);
// Remove identifier from our bank.
if (self.ids.indexOf(nipple.identifier) >= 0) {
self.ids.splice(self.ids.indexOf(nipple.identifier), 1);
}
// Clean our actives array.
if (self.actives.indexOf(nipple) >= 0) {
self.actives.splice(self.actives.indexOf(nipple), 1);
}
if (/(semi|static)/.test(opts.mode)) {
// Transfer nipple from actives to idles
// if we are in semi or static mode.
self.idles.push(nipple);
} else if (self.nipples.indexOf(nipple) >= 0) {
// Only if we are not in semi or static mode
// we can remove the instance.
self.nipples.splice(self.nipples.indexOf(nipple), 1);
}
// We unbind move and end.
self.manager.unbindDocument();
// We add back the identifier of the idle nipple;
if (/(semi|static)/.test(opts.mode)) {
self.manager.ids[removedIdentifier.id] = removedIdentifier.identifier;
}
};
// Remove destroyed nipple from the lists
Collection.prototype.onDestroyed = function(evt, nipple) {
var self = this;
if (self.nipples.indexOf(nipple) >= 0) {
self.nipples.splice(self.nipples.indexOf(nipple), 1);
}
if (self.actives.indexOf(nipple) >= 0) {
self.actives.splice(self.actives.indexOf(nipple), 1);
}
if (self.idles.indexOf(nipple) >= 0) {
self.idles.splice(self.idles.indexOf(nipple), 1);
}
if (self.ids.indexOf(nipple.identifier) >= 0) {
self.ids.splice(self.ids.indexOf(nipple.identifier), 1);
}
// Remove the identifier from our bank
self.manager.removeIdentifier(nipple.identifier);
// We unbind move and end.
self.manager.unbindDocument();
};
// Cleanly destroy the manager
Collection.prototype.destroy = function () {
var self = this;
self.unbindEvt(self.options.zone, 'start');
// Destroy nipples.
self.nipples.forEach(function(nipple) {
nipple.destroy();
});
// Clean 3DTouch intervals.
for (var i in self.pressureIntervals) {
if (self.pressureIntervals.hasOwnProperty(i)) {
clearInterval(self.pressureIntervals[i]);
}
}
// Notify the manager passing the instance
self.trigger('destroyed', self.nipples);
// We unbind move and end.
self.manager.unbindDocument();
// Unbind everything.
self.off();
};
export default Collection;
collection对象顾名思义就是一个收集器,主要就是维护了一个nipple对象的队列。负责对nipple对象进行初始化,通过不同的事件去管理当前nipple对象的状态。其定义了三种阶段的函数方法,在start阶段,创建nipple对象并对其赋值eventId,最后返回该对象。在move阶段,计算nipple对象的特定属性,如方向,角度等。end阶段就是根据当前的mode来决定是否删除该对象。
manager.js
/* global u, Super, Collection */
///////////////////////
/// MANAGER ///
///////////////////////
function Manager (options) {
var self = this;
self.ids = {};
self.index = 0;
self.collections = [];
self.config(options);
self.prepareCollections();
// Listen for resize, to reposition every joysticks
var resizeTimer;
// 创建manager对象时 绑定resize事件,获取xy轴的偏移量
u.bindEvt(window, 'resize', function (evt) {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
var pos;
var scroll = u.getScroll();
self.collections.forEach(function (collection) {
collection.forEach(function (nipple) {
pos = nipple.el.getBoundingClientRect();
nipple.position = {
x: scroll.x + pos.left,
y: scroll.y + pos.top
};
});
});
}, 100);
});
return self.collections;
};
Manager.prototype = new Super();
Manager.constructor = Manager;
// 和之前的nipple维护的数组一样,在colections数组对象中绑定manager的方法
Manager.prototype.prepareCollections = function () {
var self = this;
// Public API Preparation.
self.collections.create = self.create.bind(self);
// Listen to anything
self.collections.on = self.on.bind(self);
// Unbind general events
self.collections.off = self.off.bind(self);
// Destroy everything
self.collections.destroy = self.destroy.bind(self);
// Get any nipple
self.collections.get = function (id) {
var nipple;
self.collections.every(function (collection) {
if (nipple = collection.get(id)) {
return false;
}
return true;
});
return nipple;
};
};
// 创建collection对象
Manager.prototype.create = function (options) {
return this.createCollection(options);
};
// Collection Factory
//在manager对象中创建一个collection 对象,并添加到collections队列当中去
// 创建collection后对其绑定基本事件及回调函数
Manager.prototype.createCollection = function (options) {
var self = this;
var collection = new Collection(self, options);
self.bindCollection(collection);
self.collections.push(collection);
return collection;
};
Manager.prototype.bindCollection = function (collection) {
var self = this;
var type;
// Bubble up identified events.
var handler = function (evt, data) {
// Identify the event type with the nipples identifier.
type = evt.type + ' ' + data.id + ':' + evt.type;
self.trigger(type, data);
};
// When it gets destroyed we clean.
collection.on('destroyed', self.onDestroyed.bind(self));
// Other events that will get bubbled up.
collection.on('shown hidden rested dir plain', handler);
collection.on('dir:up dir:right dir:down dir:left', handler);
collection.on('plain:up plain:right plain:down plain:left', handler);
};
// Manager绑定 move 和end 事件
Manager.prototype.bindDocument = function () {
var self = this;
// Bind only if not already binded
if (!self.binded) {
self.bindEvt(document, 'move')
.bindEvt(document, 'end');
self.binded = true;
}
};
// Manager对象解绑move 和end 事件
Manager.prototype.unbindDocument = function (force) {
var self = this;
// If there are no touch left
// unbind the document.
if (!Object.keys(self.ids).length || force === true) {
self.unbindEvt(document, 'move')
.unbindEvt(document, 'end');
self.binded = false;
}
};
//如果事件为空那么 获取当前的index作为id,如果不为空那么就获取evt.identifier (一次触摸动作的唯一标识符) 同理 pointerId为pointEvent
// 其实就是获取当前事件的id 每次事件有一个唯一标识
// this.ids 储存的是一个对象 其中 evt的id作为key,值为对应的index,应该是触发的顺序
// 每次获取后 把最新的evt id 赋值到 latest当中去,暂时还不知道存起来的作用。
Manager.prototype.getIdentifier = function (evt) {
var id;
// If no event, simple increment
if (!evt) {
id = this.index;
} else {
// Extract identifier from event object.
// Unavailable in mouse events so replaced by latest increment.
id = evt.identifier === undefined ? evt.pointerId : evt.identifier;
if (id === undefined) {
id = this.latest || 0;
}
}
if (this.ids[id] === undefined) {
this.ids[id] = this.index;
this.index += 1;
}
// Keep the latest id used in case we are using an unidentified mouseEvent
this.latest = id;
return this.ids[id];
};
//this.ids移除事件的id
Manager.prototype.removeIdentifier = function (identifier) {
var removed = {};
for (var id in this.ids) {
if (this.ids[id] === identifier) {
removed.id = id;
removed.identifier = this.ids[id];
delete this.ids[id];
break;
}
}
return removed;
};
Manager.prototype.onmove = function (evt) {
var self = this;
self.onAny('move', evt);
return false;
};
Manager.prototype.onend = function (evt) {
var self = this;
self.onAny('end', evt);
return false;
};
Manager.prototype.oncancel = function (evt) {
var self = this;
self.onAny('end', evt);
return false;
};
// 截取processOnEnd processOnMove processOnEnd 不同的事件,定义一个函数processColl 里面是判断collection里是否存在id 如果存在就执行方法
// tm下面写得有点抽象,定义一个processEvt函数,里面对collections队列进行遍历 执行processColl u.map(evt, processEvt)其实就是相当于processEvt(evt)
Manager.prototype.onAny = function (which, evt) {
var self = this;
var id;
var processFn = 'processOn' + which.charAt(0).toUpperCase() +
which.slice(1); //processOnEnd processOnMove processOnEnd
evt = u.prepareEvent(evt);
var processColl = function (e, id, coll) {
if (coll.ids.indexOf(id) >= 0) {
coll[processFn](e);
// Mark the event to avoid cleaning it later.
e._found_ = true;
}
};
var processEvt = function (e) {
id = self.getIdentifier(e);
u.map(self.collections, processColl.bind(null, e, id));
// If the event isn t handled by any collection,
// we need to clean its identifier.
if (!e._found_) {
self.removeIdentifier(id);
}
};
u.map(evt, processEvt);
return false;
};
// Cleanly destroy the manager
Manager.prototype.destroy = function () {
var self = this;
self.unbindDocument(true);
self.ids = {};
self.index = 0;
self.collections.forEach(function(collection) {
collection.destroy();
});
self.off();
};
// When a collection gets destroyed
// we clean behind.
Manager.prototype.onDestroyed = function (evt, coll) {
var self = this;
if (self.collections.indexOf(coll) < 0) {
return false;
}
self.collections.splice(self.collections.indexOf(coll), 1);
};
manager对象所做的事情和collection类似,其维护的是collections队列,负责管理和初始化collection对象并根据实际情况调用collection定义的触控事件。(嗯,这货才是老大)
index.js
import Manager from './manager';
const factory = new Manager();
export default {
create: function (options) {
return factory.create(options);
},
factory: factory
};
index文件就没啥好说的了
总结
- 一开始看源码的时候其实是很枯燥的,但是只要熬过那开头你就能学到很多东西
- 里面的代码注释我也只是对其简单的写了一下,还有很多细节并没有描述清楚
- 即使看懂了代码但是要自己去实现还是非常有难度的,希望大家也提高一下动手能力,我就在这也立个flag吧
最后
如果大伙看的还不是很明白,别担心,这主要是我语言组织和书写表达的能力不够好,毕竟写东西向来就不是我的强项,推荐大家可以下载源码再细细品,毕竟这个库的源码并没有那么难理解