Skip to content

Instantly share code, notes, and snippets.

@mfine2
Created May 15, 2013 03:34
Show Gist options
  • Save mfine2/5581481 to your computer and use it in GitHub Desktop.
Save mfine2/5581481 to your computer and use it in GitHub Desktop.
(function(GLOBAL) {
// Phantom Limb
// ------------
// http://viewinglens.com/phantom-limb
// https://github.com/brian-c/phantom-limb
// [email protected]
"use strict";
// Default configuration
var config = {
style: true,
startOnLoad: true
};
// Apply overrides
for (var param in GLOBAL.phantomLimbConfig) {
config[param] = GLOBAL.phantomLimbConfig[param];
}
// Keep track of whether the mouse is down.
var mouseIsDown = false;
// A Finger is a representation on the screen.
// It keeps track of its position and the node that it's over.
function Finger() {
this.node = document.createElement('span');
this.node.classList.add('_phantom-limb_finger');
// Add a node per finger.
document.body.appendChild(this.node);
}
Finger.prototype = {
node: null,
x: NaN,
y: NaN,
target: null,
place: function() {
document.body.appendChild(this.node);
},
hide: function() {
this.node.style.display = 'none';
},
show: function() {
this.node.style.display = '';
},
move: function(x, y) {
if (isNaN(x) || isNaN(y)) {
this.hide();
this.target = null;
} else {
this.show();
this.node.style.left = x + 'px';
this.node.style.top = y + 'px';
this.x = x;
this.y = y;
if (!mouseIsDown) this.target = document.elementFromPoint(x, y);
}
}
};
// Instantiate the fingers we'll use in the rest of the script.
var fingers = [
new Finger(),
new Finger()
];
// Create a synthetic event from a real event and a finger.
function createMouseEvent(eventName, originalEvent, finger) {
var e = document.createEvent('MouseEvent');
e.initMouseEvent(eventName, true, true,
originalEvent.view, originalEvent.detail,
finger.x || originalEvent.screenX, finger.y || originalEvent.screenY,
finger.x || originalEvent.clientX, finger.y || originalEvent.clientY,
originalEvent.ctrlKey, originalEvent.shiftKey,
originalEvent.altKey, originalEvent.metaKey,
originalEvent.button, finger.target || originalEvent.relatedTarget
);
e.synthetic = true;
// Set this so we can match shared targets later.
e._finger = finger;
return e;
}
var startDistance = NaN;
var startAngle = NaN;
// Given a mouse event, fire a touch event for each finger.
// Add the appropriate touch-specific event properties.
function fireTouchEvents(eventName, originalEvent) {
// All touch events, including "touchend".
var events = [];
var gestures = [];
// For each finger with a target, create a touch event.
fingers.forEach(function(finger) {
if (!finger.target) return;
// Convert "ontouch*" properties and attributes to listeners.
var onEventName = 'on' + eventName;
if (onEventName in finger.target) {
console.warn('Converting `' + onEventName + '` property to event listener.', finger.target);
finger.target.addEventListener(eventName, finger.target[onEventName], false);
delete finger.target[onEventName];
}
if (finger.target.hasAttribute(onEventName)) {
console.warn('Converting `' + onEventName + '` attribute to event listener.', finger.target);
var handler = new GLOBAL.Function('event', finger.target.getAttribute(onEventName));
finger.target.addEventListener(eventName, handler, false);
finger.target.removeAttribute(onEventName);
}
// Set up a new event with the coordinates of the finger.
var touch = createMouseEvent(eventName, originalEvent, finger);
events.push(touch);
});
// Figure out scale and rotation.
if (events.length > 1) {
var x = events[0].pageX - events[1].pageX;
var y = events[0].pageY - events[1].pageY;
var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var angle = Math.atan2(x, y) * (180 / Math.PI);
var gestureName = 'gesturechange';
if (eventName === 'touchstart') {
gestureName = 'gesturestart';
startDistance = distance;
startAngle = angle;
}
if (eventName === 'touchend') gestureName = 'gestureend';
events.forEach(function(event) {
var gesture = createMouseEvent(gestureName, event, event._finger);
gestures.push(gesture);
});
events.concat(gestures).forEach(function(event) {
event.scale = distance / startDistance;
event.rotation = startAngle - angle;
});
}
// Loop through the events array and fill in each touch array.
events.forEach(function(touch) {
touch.touches = events.filter(function(e) {
return ~e.type.indexOf('touch') && e.type !== 'touchend';
});
touch.changedTouches = events.filter(function(e) {
return ~e.type.indexOf('touch') && e._finger.target === touch._finger.target;
});
touch.targetTouches = touch.changedTouches.filter(function(e) {
return ~e.type.indexOf('touch') && e.type !== 'touchend';
});
});
// Then fire the events.
events.concat(gestures).forEach(function(event, i) {
event.identifier = i;
event._finger.target.dispatchEvent(event);
});
}
// Prevent all mousedown event from doing anything.
// We'll fire one manually at touchend.
function phantomTouchStart(e) {
if (e.synthetic) return;
mouseIsDown = true;
e.preventDefault();
e.stopPropagation();
fireTouchEvents('touchstart', e);
}
// The center between two fingers
var centerX = NaN;
var centerY = NaN;
// Set each finger's position target.
// Pressing alt engages the second finger.
// Pressing shift locks the second finger's position relative to the first's.
function moveFingers(e) {
// We'll use this if the second is locked with the first.
var changeX = e.clientX - fingers[0].x || 0;
var changeY = e.clientY - fingers[0].y || 0;
// The first finger just follows the mouse.
fingers[0].move(e.clientX, e.clientY);
// TODO: Determine modifier keys independent of mouse movement.
if (e.altKey) {
// Reset the center.
if (!centerX && !centerY) {
centerX = window.innerWidth / 2;
centerY = window.innerHeight / 2;
}
// Lock the center with the first finger.
if (e.shiftKey) {
centerX += changeX;
centerY += changeY;
}
var secondX = centerX + (centerX - e.clientX);
var secondY = centerY + (centerY - e.clientY);
fingers[1].move(secondX, secondY);
} else {
// Disengage the second finger.
fingers[1].move(NaN, NaN);
// Reset the center next time the alt key is held.
centerX = NaN;
centerY = NaN;
}
}
// Prevent all mousemove events from firing.
// We'll fire one (and only one) manually at touchend.
function phantomTouchMove(e) {
if (e.synthetic) return;
e.preventDefault();
e.stopPropagation();
moveFingers(e);
if (mouseIsDown) {
fireTouchEvents('touchmove', e);
}
}
// Prevent all mouseup events from firing.
// We'll fire one manually at touchend.
function phantomTouchEnd(e) {
if (e.synthetic) return;
mouseIsDown = false;
e.preventDefault();
e.stopPropagation();
fireTouchEvents('touchend', e);
fingers.forEach(function(finger) {
if (!finger.target) return;
// Mobile Safari moves all the mouse event to fire after the touchend event.
finger.target.dispatchEvent(createMouseEvent('mouseover', e, finger));
finger.target.dispatchEvent(createMouseEvent('mousemove', e, finger));
finger.target.dispatchEvent(createMouseEvent('mousedown', e, finger));
// TODO: These two only fire if content didn't change. How can we tell?
finger.target.dispatchEvent(createMouseEvent('mouseup', e, finger));
finger.target.dispatchEvent(createMouseEvent('click', e, finger));
});
}
// Prevent clicks. We'll handle them manually.
function phantomClick(e) {
if (e.synthetic) return;
e.preventDefault();
e.stopPropagation();
}
// Detect keyup, exit when esc.
function phantomKeyUp(e) {
if (e.keyCode == 27) stop();
}
// Not entirely proud of this, but I can't serve CSS from GitHub
// and I want the bookmarklet to be as simple as possible.
var defaultCSS = ([
'._phantom-limb,',
'._phantom-limb a {',
'cursor: none !important;',
'}',
'._phantom-limb_finger {',
'background: rgba(128, 128, 128, 0.5);',
'border: 2px solid rgb(128, 128, 128);',
'border-radius: 50%;',
'display: none;',
'height: 44px;',
'margin: -22px 0 0 -22px;',
'pointer-events: none;',
'position: fixed;',
'width: 44px;',
'z-index: 999999' +
'}',
'._phantom-limb ._phantom-limb_finger {',
'display: block;',
'}'
]).join('\n');
var styleTag = document.createElement('style');
styleTag.id = '_phantom-limb_default-style';
styleTag.innerHTML = defaultCSS;
if (config.style) document.querySelector('head').appendChild(styleTag);
// On/off switch
function start() {
document.addEventListener('mousedown', phantomTouchStart, true);
document.addEventListener('mousemove', phantomTouchMove, true);
document.addEventListener('mouseup', phantomTouchEnd, true);
document.addEventListener('click', phantomClick, true);
document.addEventListener('keyup', phantomKeyUp, true);
document.documentElement.classList.add('_phantom-limb');
}
function stop() {
document.removeEventListener('mousedown', phantomTouchStart, true);
document.removeEventListener('mousemove', phantomTouchMove, true);
document.removeEventListener('mouseup', phantomTouchEnd, true);
document.removeEventListener('click', phantomClick, true);
document.removeEventListener('keyup', phantomKeyUp, true);
document.documentElement.classList.remove('_phantom-limb');
}
var phantomLimb = {
start: start,
stop: stop
};
// TODO: Test these. I'm really just guessing.
if (typeof GLOBAL.define === 'function') {
GLOBAL.define(phantomLimb);
} else if (typeof GLOBAL.exports !== 'undefined') {
GLOBAL.exports = phantomLimb;
} else {
GLOBAL.phantomLimb = phantomLimb;
}
if (config.startOnLoad) start();
}(this));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment