diff --git a/raspberry/roberto/static/js/nipplejs.js b/raspberry/roberto/static/js/nipplejs.js new file mode 100644 index 0000000..d404c12 --- /dev/null +++ b/raspberry/roberto/static/js/nipplejs.js @@ -0,0 +1,1474 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.nipplejs = f()}})(function(){var define,module,exports; +'use strict'; + +// Constants +var isTouch = !!('ontouchstart' in window); +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; +} + +/////////////////////// +/// UTILS /// +/////////////////////// + +var u = {}; +u.distance = function (p1, p2) { + var dx = p2.x - p1.x; + var dy = p2.y - p1.y; + + return Math.sqrt((dx * dx) + (dy * dy)); +}; + +u.angle = function(p1, p2) { + var dx = p2.x - p1.x; + var dy = p2.y - p1.y; + + return u.degrees(Math.atan2(dy, dx)); +}; + +u.findCoord = function(p, d, a) { + var b = {x: 0, y: 0}; + a = u.radians(a); + b.x = p.x - d * Math.cos(a); + b.y = p.y - d * Math.sin(a); + return b; +}; + +u.radians = function(a) { + return a * (Math.PI / 180); +}; + +u.degrees = function(a) { + return a * (180 / Math.PI); +}; + +u.bindEvt = function (el, arg, handler) { + var types = arg.split(/[ ,]+/g); + var type; + for (var i = 0; i < types.length; i += 1) { + type = types[i]; + if (el.addEventListener) { + el.addEventListener(type, handler, false); + } else if (el.attachEvent) { + el.attachEvent(type, handler); + } + } +}; + +u.unbindEvt = function (el, arg, handler) { + var types = arg.split(/[ ,]+/g); + var type; + for (var 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); + } + } +}; + +u.trigger = function (el, type, data) { + var evt = new CustomEvent(type, data); + el.dispatchEvent(evt); +}; + +u.prepareEvent = function (evt) { + evt.preventDefault(); + return evt.type.match(/^touch/) ? evt.changedTouches : evt; +}; + +u.getScroll = function () { + var x = (window.pageXOffset !== undefined) ? + window.pageXOffset : + (document.documentElement || document.body.parentNode || document.body) + .scrollLeft; + + var y = (window.pageYOffset !== undefined) ? + window.pageYOffset : + (document.documentElement || document.body.parentNode || document.body) + .scrollTop; + return { + x: x, + y: y + }; +}; + +u.applyPosition = function (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'; + } +}; + +u.getTransitionStyle = function (property, values, time) { + var obj = u.configStylePropertyObject(property); + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (typeof values === 'string') { + obj[i] = values + ' ' + time; + } else { + var st = ''; + for (var j = 0, max = values.length; j < max; j += 1) { + st += values[j] + ' ' + time + ', '; + } + obj[i] = st.slice(0, -2); + } + } + } + return obj; +}; + +u.getVendorStyle = function (property, value) { + var obj = u.configStylePropertyObject(property); + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + obj[i] = value; + } + } + return obj; +}; + +u.configStylePropertyObject = function (prop) { + var obj = {}; + obj[prop] = ''; + var vendors = ['webkit', 'Moz', 'o']; + vendors.forEach(function (vendor) { + obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = ''; + }); + return obj; +}; + +u.extend = function (objA, objB) { + for (var i in objB) { + if (objB.hasOwnProperty(i)) { + objA[i] = objB[i]; + } + } + return objA; +}; + +// Overwrite only what's already present +u.safeExtend = function (objA, objB) { + var obj = {}; + for (var 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. +u.map = function (ar, fn) { + if (ar.length) { + for (var i = 0, max = ar.length; i < max; i += 1) { + fn(ar[i]); + } + } else { + fn(ar); + } +}; + +/////////////////////// +/// SUPER CLASS /// +/////////////////////// + +function Super () {}; + +// Basic event system. +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 { + 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; +}; + +/////////////////////// +/// 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), + 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; +}; + +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; + } + + 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. +Nipple.prototype.stylize = function () { + if (this.options.dataOnly) { + return this; + } + var animTime = this.options.fadeTime + 'ms'; + 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' + }; + + u.extend(styles.el, transitStyle); + u.extend(styles.back, borderStyle); + u.extend(styles.front, borderStyle); + + 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.restPosition(); + } + + return self; +}; + +Nipple.prototype.restPosition = function (cb) { + var self = this; + self.frontPosition = { + x: 0, + y: 0 + }; + 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; + 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 = {}; + for (var 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 (var 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); + } + } + return obj; +}; + +/* global Nipple, Super */ + +/////////////////////////// +/// THE COLLECTION /// +/////////////////////////// + +function Collection (manager, options) { + var self = this; + self.nipples = []; + self.idles = []; + self.actives = []; + self.ids = []; + self.pressureIntervals = {}; + self.manager = manager; + self.id = Collection.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 + }; + + self.config(options); + + // Overwrites + if (self.options.mode === 'static' || self.options.mode === 'semi') { + self.options.multitouch = false; + } + + if (!self.options.multitouch) { + self.options.maxNumberOfNipples = 1; + } + + self.updateBox(); + 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. + 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. + 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 nipple's 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.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; + evt = u.prepareEvent(evt); + + // Update the box position + self.updateBox(); + + var process = function (touch) { + // If we can create new nipples + // meaning we don't have more active nipples than we should. + 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.prototype.getOrCreate = function (identifier, position) { + var self = this; + var opts = self.options; + var nipple; + + // If we're 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're in semi mode, we need to create one. + return self.createNipple(position, identifier); + } + + console.warn('Coudln\'t find the needed nipple.'); + return false; + } + // In dynamic, we create a new one. + nipple = self.createNipple(position, identifier); + return nipple; +}; + +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 (!nipple) { + // This is here just for safety. + // It shouldn't happen. + console.error('Found zombie joystick with ID ' + identifier); + self.manager.removeIdentifier(identifier); + return; + } + + nipple.identifier = identifier; + + var size = nipple.options.size / 2; + var pos = { + x: evt.pageX, + y: evt.pageY + }; + + var dist = u.distance(pos, nipple.position); + var angle = u.angle(pos, nipple.position); + var rAngle = u.radians(angle); + var force = dist / size; + + // 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 + + if (opts.lockX){ + yPosition = 0 + } + if (opts.lockY) { + xPosition = 0 + } + + 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 + }, + 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); +}; + +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're in semi or static mode. + self.idles.push(nipple); + } else if (self.nipples.indexOf(nipple) >= 0) { + // Only if we're 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(); +}; + +/* 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; + 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; + +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; + }; +}; + +Manager.prototype.create = function (options) { + return this.createCollection(options); +}; + +// Collection Factory +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 nipple's 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.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.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; + } +}; + +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're using an unidentified mouseEvent + this.latest = id; + return 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; +}; + +Manager.prototype.onAny = function (which, evt) { + var self = this; + var id; + var processFn = 'processOn' + which.charAt(0).toUpperCase() + + which.slice(1); + 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); +}; + +var factory = new Manager(); +return { + create: function (options) { + return factory.create(options); + }, + factory: factory +}; + +}); diff --git a/raspberry/roberto/views/frontend/templates/gamepad.js b/raspberry/roberto/views/frontend/templates/gamepad.js index 5f3afd0..27a2068 100644 --- a/raspberry/roberto/views/frontend/templates/gamepad.js +++ b/raspberry/roberto/views/frontend/templates/gamepad.js @@ -95,56 +95,13 @@ if (haveEvents) { } // Virtual Joystick - var joystick = new VirtualJoystick({ - container: document.getElementById('controls'), - mouseSupport: true, - stationaryBase: true, - baseX: 250, - baseY: window.innerHeight-300, - limitStickTravel: true, - stickRadius: 120, - strokeStyle: 'cyan' - }); - - var joystick2 = new VirtualJoystick({ - container: document.getElementById('controls'), - mouseSupport: true, - stationaryBase: true, - baseX: window.innerWidth-250, - baseY: window.innerHeight-300, - limitStickTravel: true, - stickRadius: 120, - strokeStyle: 'orange' - }); - - joystick.addEventListener('touchStartValidation', function(event){ - var touch = event.changedTouches[0]; - if( touch.pageX >= window.innerWidth/2 ) return false; - return true - }); - joystick.addEventListener('mouseStartValidation', function(event){ - if( event.clientX >= window.innerWidth/2 ) return false; - return true - }); - - - joystick2.addEventListener('touchStartValidation', function(event){ - var touch = event.changedTouches[0]; - if( touch.pageX < window.innerWidth/2 ) return false; - return true - }); - joystick2.addEventListener('mouseStartValidation', function(event){ - if( event.clientX < window.innerWidth/2 ) return false; - return true - }); + var joystick = nipplejs.create({ + zone: document.getElementById('controls'), + color: 'blue' + }); /* - joystick2.addEventListener('touchStart', function(){ - console.log('fire') - }) -*/ - setInterval(function(){ - if (joystick && joystick2) { + if (joystick) { if (joystick._pressed === true || joystick2._pressed === true) { var dx = joystick.deltaX() / joystick._stickRadius; var dy = joystick.deltaY() / joystick._stickRadius; @@ -154,8 +111,8 @@ setInterval(function(){ } } }, 50); - +*/ window.addEventListener("orientationchange", function(event) { - joystick._updatePositionOfContainer(); - joystick2._updatePositionOfContainer(); + //joystick._updatePositionOfContainer(); + //joystick2._updatePositionOfContainer(); }); diff --git a/raspberry/roberto/views/frontend/templates/index.html b/raspberry/roberto/views/frontend/templates/index.html index 3ea082d..df044f2 100644 --- a/raspberry/roberto/views/frontend/templates/index.html +++ b/raspberry/roberto/views/frontend/templates/index.html @@ -76,6 +76,7 @@ + -