Files
ExoMy_Cuno/ExoMy_Software-master/gui/node_modules/nipplejs/src/collection.js
T
2026-05-18 19:49:40 +02:00

580 lines
16 KiB
JavaScript

import Nipple from './nipple';
import Super from './super';
import * as u from './utils';
///////////////////////////
/// 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,
dynamicPage: 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;
var origEvt = evt;
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);
}
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 doesn't 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.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);
}
// eslint-disable-next-line no-console
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 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 shouldn't 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);
};
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();
};
export default Collection;