Track mouse speed with JS - javascript

What's the best way to track the mouse speed with plain JS/JQuery? I'd like to track how fast a user moves the mouse in all directions (up/down/left/right).

Sparklines has a nifty example of tracking mouse movement and graphing it. Their code is available in the source of their site starting at line 315.
Simple and effective.
Here is the code:
var mrefreshinterval = 500; // update display every 500ms
var lastmousex=-1;
var lastmousey=-1;
var lastmousetime;
var mousetravel = 0;
$('html').mousemove(function(e) {
var mousex = e.pageX;
var mousey = e.pageY;
if (lastmousex > -1)
mousetravel += Math.max( Math.abs(mousex-lastmousex), Math.abs(mousey-lastmousey) );
lastmousex = mousex;
lastmousey = mousey;
});

var timestamp = null;
var lastMouseX = null;
var lastMouseY = null;
document.body.addEventListener("mousemove", function(e) {
if (timestamp === null) {
timestamp = Date.now();
lastMouseX = e.screenX;
lastMouseY = e.screenY;
return;
}
var now = Date.now();
var dt = now - timestamp;
var dx = e.screenX - lastMouseX;
var dy = e.screenY - lastMouseY;
var speedX = Math.round(dx / dt * 100);
var speedY = Math.round(dy / dt * 100);
timestamp = now;
lastMouseX = e.screenX;
lastMouseY = e.screenY;
});

With current modern browser we can now use movementX or movementY to detect mouse's movement speed. Before you want to use it you should see the compatibility table because older browser will have a prefix like webkitMovementX.
document.addEventListener("mousemove", function(ev){
console.log(`Movement X: ${ev.movementX}, Y: ${ev.movementY}`);
}, false);
The result above is not an average speed like pixel/second but it's total movement between triggered mousemove event. If you need px/s then you can do it like below:
var totalX = 0;
var totalY = 0;
var moveX = 0;
var moveY = 0;
document.addEventListener("mousemove", function(ev){
totalX += Math.abs(ev.movementX);
totalY += Math.abs(ev.movementY);
moveX += ev.movementX;
moveY += ev.movementY;
}, false);
setInterval(function(){
console.log(`Speed X: ${totalX}px/s, Y: ${totalY}px/s`);
console.log(`Movement X: ${moveX}px/s, Y: ${moveY}px/s`);
moveX = moveY = totalX = totalY = 0;
}, 1000);
Negative number represent movement to the left or top, while positive represent movement to the bottom or right direction.

Same way you get speed for anything else:
speed = distance / time
acceleration = speed / time
And use:
$(document).mousemove(function(e){
var xcoord = e.pageX;
var ycoord = e.pageY;
});
To get the mouse coordinates whenever the mouse moves.

This is a method to counter the fact you could start tracking, pause and then move your finger or mouse very quickly (suppose a sudden flick on a touch screen).
var time = 200
var tracker = setInterval(function(){
historicTouchX = touchX;
}, time);
document.addEventListener("touchmove", function(){
speed = (historicTouchX - touchX) / time;
console.log(Math.abs(speed));
}, false);
I have done this with only the touchX in this example. The idea is to take a snapshot of the x position every 200 milliseconds, and then take that from the current position then divide by the 200 (speed = distance / time). This would keep a fresh update on the speed. The time is milliseconds and the output would be the number of pixels traveled per 200 milliseconds.

I also had a requirement to find the acceleration, speed, movement of the mouse. Below is the code which is implemented for the react application. Through this we were able to find the movement, speed, max speed, acceleration, maximum acceleration of the mouse.
let previousEvent, currentEvent;
let maxSpeed = 0, previousSpeed = 0, speed = 0, maxPositiveAcc = 0, maxNegativeAcc = 0;
componentDidMount() {
document.addEventListener('mousemove', (event) => {
currentEvent = event
});
setInterval(function () {
if (currentEvent && previousEvent) {
let movementX = Math.abs(currentEvent.pageX - previousEvent.pageX);
let movementY = Math.abs(currentEvent.pageY - previousEvent.pageY);
let movement = Math.sqrt(movementX * movementX + movementY * movementY);
//Dividing by 100 since the setInterval function is called every 100ms
speed = 10 * movement;
maxSpeed = Math.round(speed > maxSpeed ? (maxSpeed = speed) : maxSpeed);
let acceleration = 10 * (speed - previousSpeed);
if (acceleration > 0) {
maxPositiveAcceleration = Math.round(acceleration > maxPositiveAcc ? (maxPositiveAcc = acceleration) : maxPositiveAcc);
} else {
maxNegativeAcceleration = Math.round(acceleration < maxNegativeAcc ? (maxNegativeAcc = acceleration) : maxNegativeAcc);
}
}
previousEvent = currentEvent
previousSpeed = speed;
}, 100);
}

I'm looking for a way to track mouse speed as well. I found this video on Youtube https://www.youtube.com/watch?v=Lrfmu9V_foE. You can see how to track mouse speed with mousemove event once has defined previous mouse event and current mouse event.
Anyways, I want to store the speed as a value to use elsewhere but don't know how to.

Related

translating an element goes back to origin place

I'm trying to make an element moves when the mouse moves in a random 340° direction excluding the 20° of the cursor so the element won't move to the cursor but when I translate It always goes back to the origin place as if there were no translating. here's the code:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>SVG</title>
</head>
<body>
<svg id="m" width="40" height="40">
<circle cx="20" cy="20" r="20" fill="red" stroke="red" stroke-width="1"/>
</svg>
<script>
let m = document.getElementById("m");
let angle = Math.floor(Math.random() * 340) * Math.PI / 180;
var timestamp, lastMouseX, lastMouseY = null;
let setCursorPosition = function(e) {
//mousespeed
if (timestamp === null) {
timestamp = Date.now();
lastMouseX = e.screenX;
lastMouseY = e.screenY;
return;
}
var now = Date.now();
var dt = now - timestamp;
var dx = e.screenX - lastMouseX;
var dy = e.screenY - lastMouseY;
var speedX = Math.round(dx / dt * 100);
var speedY = Math.round(dy / dt * 100);
timestamp = now;
lastMouseX = e.screenX;
lastMouseY = e.screenY;
//
m.style.transform = "translate(" + Math.cos(angle)*speedX + "px," + Math.sin(angle)*speedY + "px)";
};
document.addEventListener("mousemove", e => setCursorPosition(e));
</script>
</body>
</html>
thanks
also tell me if the implementation of the direction of the element is just like I wanted or no, I have a feeling that it's wrong
I'm not entirely sure what you're going for here, but I played around with you code and I think I got it to behave how you want it to. The first thing I noticed (very minor), was that this line:
var timestamp, lastMouseX, lastMouseY = null;
should be:
var timestamp = null, lastMouseX = null, lastMouseY = null;
Otherwise you are not actually setting timestamp and lastMouseX initially.
More importantly, the reason your element continues to return to its origin is that it's getting very small mouse inputs as you decelerate your mouse so speedX and speedY are set to low values at the end of each mouse move.
To fix this I added a this line right before your transform command:
if (Math.abs(speedX) < 40 && Math.abs(speedY) < 40) return;
Also to make the movement a little smoother I also added a throttle to your event listener so that setCursorPosition isn't called quite so often (reduces jittery movement), and I added a transition in the styles.
function throttle(func, interval) {
var lastCall = 0;
return function() {
let curTime = Date.now();
if (lastCall + interval < curTime) {
lastCall = curTime;
return func.apply(this, arguments);
}
};
}
document.addEventListener("mousemove", throttle(setCursorPosition, 20));
/* ^ replace original document.addEventListener() with this */
#m {
transition: transform 140ms;
margin: 200px; //just so I could always see it on the screen
}
The numbers I used for the min speed, throttle, and transition were purely experimental so feel free to play around with them to your liking.

Get mouse moving speed

I didn't get a exact solution/calculation from stackoverflow so i created a question
var timestamp = null;
var mY = 0;
$(document).mousemove(function(e) {
var now = Date.now();
currentmY = e.pageY;
mY = e.pageY;
timestamp = now;
});
I need to get a speed value when mouse move vertical angle.
https://jsfiddle.net/58tjr9o1/
The speed is simply the distance divided by the time it took:
speed = distance / time
The distance is currentmY - mY, while the time is now - timestamp. So in the end, you get:
var timestamp = 0;
var mY = 0;
$(document).mousemove(function(e) {
var now = Date.now();
currentmY = e.screenY;
var dt = now - timestamp;
var distance = Math.abs(currentmY - mY);
var speed = Math.round(distance / dt * 1000);
console.log(dt, distance, speed);
document.getElementById("speed").innerHTML = speed;
mY = currentmY;
timestamp = now;
});
Note the * 1000, since the timestamp is in milliseconds. The speed is here in pixels/second.
See this updated fiddle.
Following code will continuously update mouse's vertical movement speed in the span with id = "update-speed". Code is self explanatory and easy to understand, it just saved current position, previous position, current time and previous time and then calculates the speed using this formula (speed = (pos2 - pos1) / (time2 - time1).
HTML
<span id="update-speed">Update speed</span>
JS
var prev_time = new Date();
var prev_pos_y = 0;
$(document).mousemove(function(e) {
var now = new Date();
current_pos_y = e.pageY;
time_interval = now.getTime() - prev_time.getTime();
if(time_interval != 0)
{
speed = ( Math.abs(current_pos_y - prev_pos_y) / time_interval );
}
else
speed = 0;
console.log(speed);
$('#update-speed').text(speed);
prev_time = now;
prev_pos_y = current_pos_y;
});

Controlling state

getMouseXY = function(e) {
tempX = e.pageX
tempY = e.pageY
if (tempX < 0){tempX = 0}
if (tempY < 0){tempY = 0}
document.getElementById("circle1").style.top = (tempY - 25) + "px";
document.getElementById("circle1").style.left = (tempX - 25) + "px";
return true
}
document.onmousemove = getMouseXY;
trackCircle = function() {
document.getElementById("circle1").style.top = "10px";
document.getElementById("circle1").style.left = "10px";
}
document.getElementById("circle1").addEventListener("click", trackCircle);
<div id="circle1" style="width:50px;height:50px;background-color:orange;border-radius:50px;position:absolute;"></div>
The idea is the circle tracks your mouse wherever it goes, until you click, then it goes back to its resting spot. The problem is, when I move the mouse again it undoes the resting position, and returns to tracking the document.onmousemove The plan is for it to only resume tracking, once in rest, after you re-hover "circle1" ... and then resumes tracking with document.onmousemove
I know I could call something like this instead of document.onmousemove:
document.getElementById("circle1").addEventListener('mousemove', getMouseXY);
But the "is the mouse hovering over this specific element?" event is to imprecise and you end up losing your tracking a lot when you move the mouse around quickly.
Seems like I need to build some kind of state machine where its either trackingON or trackingOFF. Where trackingOFF is triggered by clicking the circle and trackingON is triggered by hovering the circle (but then the actual tracking uses document.onmousemove
You need a flag to specify that "I have clicked, stop tracking my mouse" to be set when you click and a check to say "I have moved my mouse over the circle, resume tracking me"
Here is the updated fiddle: http://jsfiddle.net/ub9v9p0j/8/
and the actual code:
var stopTracking = false;
getMouseXY = function(e) {
tempX = e.pageX
tempY = e.pageY
var circle = document.querySelector('#circle1');
var r = circle.offsetWidth / 2;
if (tempX < 0){tempX = 0}
if (tempY < 0){tempY = 0}
if (stopTracking) {
if (Math.pow(tempX - circle.offsetLeft - r, 2) + Math.pow(tempY - circle.offsetTop - r, 2) < r * r) {
stopTracking = false;
} else {
return;
}
}
document.getElementById("circle1").style.top = (tempY - 25) + "px";
document.getElementById("circle1").style.left = (tempX - 25) + "px";
return true
}
document.onmousemove = getMouseXY;
trackCircle = function() {
document.getElementById("circle1").style.top = "10px";
document.getElementById("circle1").style.left = "10px";
stopTracking = true;
}
document.getElementById("circle1").addEventListener("click", trackCircle);

Raphael transform object diagonally and infinite setIntervals

I'm working on a small animation where the user drags a circle and the circle returns back to the starting point. I figured out a way to have the circle return to the starting point. The only problem is that it will hit one of the sides of the frame before returning. Is it possible for it to go straight back (follow the path of a line drawn between the shape and starting point).
The other problem is that my setInterval doesn't want to stop. If you try pulling it a second time it would pull it back before you release your mouse. It also seems to speed up after every time. I have tried using a while loop with a timer but the results weren't as good. Is this fixable?
var paper = Raphael(0, 0, 320, 200);
//var path = paper.path("M10 10L40 40").attr({stoke:'#000000'});
//var pathArray = path.attr("path");
var circle = paper.circle(50, 50, 20);
var newX;
var newY;
circle.attr("fill", "#f00");
circle.attr("stroke", "#fff");
var start = function () {
this.attr({cx: 50, cy: 50});
this.cx = this.attr("cx"),
this.cy = this.attr("cy");
},
move = function (dx, dy) {
var X = this.cx + dx,
Y = this.cy + dy;
this.attr({cx: X, cy: Y});
},
up = function () {
setInterval(function () {
if(circle.attr('cx') > 50){
circle.attr({cx : (circle.attr('cx') - 1)});
} else if (circle.attr('cx') < 50){
circle.attr({cx : (circle.attr('cx') + 1)});
}
if(circle.attr('cy') > 50){
circle.attr({cy : (circle.attr('cy') - 1)});
} else if (circle.attr('cy') < 50){
circle.attr({cy : (circle.attr('cy') + 1)});
}
path.attr({path: pathArray});
},2);
};
circle.drag(move, start, up);
Here's the Jfiddle: http://jsfiddle.net/Uznp2/
Thanks alot :D
I modified the "up" function to the one below
up = function () {
//starting x, y of circle to go back to
var interval = 1000;
var startingPointX = 50;
var startingPointY = 50;
var centerX = this.getBBox().x + (this.attr("r")/2);
var centerY = this.getBBox().y + (this.attr("r")/2);
var transX = (centerX - startingPointX) * -1;
var transY = (centerY - startingPointY) * -1;
this.animate({transform: "...T"+transX+", "+transY}, interval);
};
and the "start" function as follows:
var start = function () {
this.cx = this.attr("cx"),
this.cy = this.attr("cy");
}
Is this the behavior you are looking for? Sorry if I misunderstood the question.
If the circle need to get back to its initial position post drag, we can achieve that via simple animation using transform attribute.
// Assuming that (50,50) is the location the circle prior to drag-move (as seen in the code provided)
// The animation is set to execute in 1000 milliseconds, using the easing function of 'easeIn'.
up = function () {
circle.animate({transform: 'T50,50'}, 1000, 'easeIn');
};
Hope this helps.

Simplest way to detect a pinch

This is a WEB APP not a native app. Please no Objective-C NS commands.
So I need to detect 'pinch' events on iOS. Problem is every plugin or method I see for doing gestures or multi-touch events, is (usually) with jQuery and is a whole additional pluggin for every gesture under the sun. My application is huge, and I am very sensitive to deadwood in my code. All I need is to detect a pinch, and using something like jGesture is just way to bloated for my simple needs.
Additionally, I have a limited understanding of how to detect a pinch manually. I can get the position of both fingers, can't seem to get the mix right to detect this. Does anyone have a simple snippet that JUST detects pinch?
Think about what a pinch event is: two fingers on an element, moving toward or away from each other.
Gesture events are, to my knowledge, a fairly new standard, so probably the safest way to go about this is to use touch events like so:
(ontouchstart event)
if (e.touches.length === 2) {
scaling = true;
pinchStart(e);
}
(ontouchmove event)
if (scaling) {
pinchMove(e);
}
(ontouchend event)
if (scaling) {
pinchEnd(e);
scaling = false;
}
To get the distance between the two fingers, use the hypot function:
var dist = Math.hypot(
e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY);
You want to use the gesturestart, gesturechange, and gestureend events. These get triggered any time 2 or more fingers touch the screen.
Depending on what you need to do with the pinch gesture, your approach will need to be adjusted. The scale multiplier can be examined to determine how dramatic the user's pinch gesture was. See Apple's TouchEvent documentation for details about how the scale property will behave.
node.addEventListener('gestureend', function(e) {
if (e.scale < 1.0) {
// User moved fingers closer together
} else if (e.scale > 1.0) {
// User moved fingers further apart
}
}, false);
You could also intercept the gesturechange event to detect a pinch as it happens if you need it to make your app feel more responsive.
Hammer.js all the way! It handles "transforms" (pinches).
http://eightmedia.github.com/hammer.js/
But if you wish to implement it youself, i think that Jeffrey's answer is pretty solid.
Unfortunately, detecting pinch gestures across browsers is a not as simple as one would hope, but HammerJS makes it a lot easier!
Check out the Pinch Zoom and Pan with HammerJS demo. This example has been tested on Android, iOS and Windows Phone.
You can find the source code at Pinch Zoom and Pan with HammerJS.
For your convenience, here is the source code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<title>Pinch Zoom</title>
</head>
<body>
<div>
<div style="height:150px;background-color:#eeeeee">
Ignore this area. Space is needed to test on the iPhone simulator as pinch simulation on the
iPhone simulator requires the target to be near the middle of the screen and we only respect
touch events in the image area. This space is not needed in production.
</div>
<style>
.pinch-zoom-container {
overflow: hidden;
height: 300px;
}
.pinch-zoom-image {
width: 100%;
}
</style>
<script src="https://hammerjs.github.io/dist/hammer.js"></script>
<script>
var MIN_SCALE = 1; // 1=scaling when first loaded
var MAX_SCALE = 64;
// HammerJS fires "pinch" and "pan" events that are cumulative in nature and not
// deltas. Therefore, we need to store the "last" values of scale, x and y so that we can
// adjust the UI accordingly. It isn't until the "pinchend" and "panend" events are received
// that we can set the "last" values.
// Our "raw" coordinates are not scaled. This allows us to only have to modify our stored
// coordinates when the UI is updated. It also simplifies our calculations as these
// coordinates are without respect to the current scale.
var imgWidth = null;
var imgHeight = null;
var viewportWidth = null;
var viewportHeight = null;
var scale = null;
var lastScale = null;
var container = null;
var img = null;
var x = 0;
var lastX = 0;
var y = 0;
var lastY = 0;
var pinchCenter = null;
// We need to disable the following event handlers so that the browser doesn't try to
// automatically handle our image drag gestures.
var disableImgEventHandlers = function () {
var events = ['onclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',
'onmouseup', 'ondblclick', 'onfocus', 'onblur'];
events.forEach(function (event) {
img[event] = function () {
return false;
};
});
};
// Traverse the DOM to calculate the absolute position of an element
var absolutePosition = function (el) {
var x = 0,
y = 0;
while (el !== null) {
x += el.offsetLeft;
y += el.offsetTop;
el = el.offsetParent;
}
return { x: x, y: y };
};
var restrictScale = function (scale) {
if (scale < MIN_SCALE) {
scale = MIN_SCALE;
} else if (scale > MAX_SCALE) {
scale = MAX_SCALE;
}
return scale;
};
var restrictRawPos = function (pos, viewportDim, imgDim) {
if (pos < viewportDim/scale - imgDim) { // too far left/up?
pos = viewportDim/scale - imgDim;
} else if (pos > 0) { // too far right/down?
pos = 0;
}
return pos;
};
var updateLastPos = function (deltaX, deltaY) {
lastX = x;
lastY = y;
};
var translate = function (deltaX, deltaY) {
// We restrict to the min of the viewport width/height or current width/height as the
// current width/height may be smaller than the viewport width/height
var newX = restrictRawPos(lastX + deltaX/scale,
Math.min(viewportWidth, curWidth), imgWidth);
x = newX;
img.style.marginLeft = Math.ceil(newX*scale) + 'px';
var newY = restrictRawPos(lastY + deltaY/scale,
Math.min(viewportHeight, curHeight), imgHeight);
y = newY;
img.style.marginTop = Math.ceil(newY*scale) + 'px';
};
var zoom = function (scaleBy) {
scale = restrictScale(lastScale*scaleBy);
curWidth = imgWidth*scale;
curHeight = imgHeight*scale;
img.style.width = Math.ceil(curWidth) + 'px';
img.style.height = Math.ceil(curHeight) + 'px';
// Adjust margins to make sure that we aren't out of bounds
translate(0, 0);
};
var rawCenter = function (e) {
var pos = absolutePosition(container);
// We need to account for the scroll position
var scrollLeft = window.pageXOffset ? window.pageXOffset : document.body.scrollLeft;
var scrollTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop;
var zoomX = -x + (e.center.x - pos.x + scrollLeft)/scale;
var zoomY = -y + (e.center.y - pos.y + scrollTop)/scale;
return { x: zoomX, y: zoomY };
};
var updateLastScale = function () {
lastScale = scale;
};
var zoomAround = function (scaleBy, rawZoomX, rawZoomY, doNotUpdateLast) {
// Zoom
zoom(scaleBy);
// New raw center of viewport
var rawCenterX = -x + Math.min(viewportWidth, curWidth)/2/scale;
var rawCenterY = -y + Math.min(viewportHeight, curHeight)/2/scale;
// Delta
var deltaX = (rawCenterX - rawZoomX)*scale;
var deltaY = (rawCenterY - rawZoomY)*scale;
// Translate back to zoom center
translate(deltaX, deltaY);
if (!doNotUpdateLast) {
updateLastScale();
updateLastPos();
}
};
var zoomCenter = function (scaleBy) {
// Center of viewport
var zoomX = -x + Math.min(viewportWidth, curWidth)/2/scale;
var zoomY = -y + Math.min(viewportHeight, curHeight)/2/scale;
zoomAround(scaleBy, zoomX, zoomY);
};
var zoomIn = function () {
zoomCenter(2);
};
var zoomOut = function () {
zoomCenter(1/2);
};
var onLoad = function () {
img = document.getElementById('pinch-zoom-image-id');
container = img.parentElement;
disableImgEventHandlers();
imgWidth = img.width;
imgHeight = img.height;
viewportWidth = img.offsetWidth;
scale = viewportWidth/imgWidth;
lastScale = scale;
viewportHeight = img.parentElement.offsetHeight;
curWidth = imgWidth*scale;
curHeight = imgHeight*scale;
var hammer = new Hammer(container, {
domEvents: true
});
hammer.get('pinch').set({
enable: true
});
hammer.on('pan', function (e) {
translate(e.deltaX, e.deltaY);
});
hammer.on('panend', function (e) {
updateLastPos();
});
hammer.on('pinch', function (e) {
// We only calculate the pinch center on the first pinch event as we want the center to
// stay consistent during the entire pinch
if (pinchCenter === null) {
pinchCenter = rawCenter(e);
var offsetX = pinchCenter.x*scale - (-x*scale + Math.min(viewportWidth, curWidth)/2);
var offsetY = pinchCenter.y*scale - (-y*scale + Math.min(viewportHeight, curHeight)/2);
pinchCenterOffset = { x: offsetX, y: offsetY };
}
// When the user pinch zooms, she/he expects the pinch center to remain in the same
// relative location of the screen. To achieve this, the raw zoom center is calculated by
// first storing the pinch center and the scaled offset to the current center of the
// image. The new scale is then used to calculate the zoom center. This has the effect of
// actually translating the zoom center on each pinch zoom event.
var newScale = restrictScale(scale*e.scale);
var zoomX = pinchCenter.x*newScale - pinchCenterOffset.x;
var zoomY = pinchCenter.y*newScale - pinchCenterOffset.y;
var zoomCenter = { x: zoomX/newScale, y: zoomY/newScale };
zoomAround(e.scale, zoomCenter.x, zoomCenter.y, true);
});
hammer.on('pinchend', function (e) {
updateLastScale();
updateLastPos();
pinchCenter = null;
});
hammer.on('doubletap', function (e) {
var c = rawCenter(e);
zoomAround(2, c.x, c.y);
});
};
</script>
<button onclick="zoomIn()">Zoom In</button>
<button onclick="zoomOut()">Zoom Out</button>
<div class="pinch-zoom-container">
<img id="pinch-zoom-image-id" class="pinch-zoom-image" onload="onLoad()"
src="https://hammerjs.github.io/assets/img/pano-1.jpg">
</div>
</div>
</body>
</html>
detect two fingers pinch zoom on any element, easy and w/o hassle with 3rd party libs like Hammer.js (beware, hammer has issues with scrolling!)
function onScale(el, callback) {
let hypo = undefined;
el.addEventListener('touchmove', function(event) {
if (event.targetTouches.length === 2) {
let hypo1 = Math.hypot((event.targetTouches[0].pageX - event.targetTouches[1].pageX),
(event.targetTouches[0].pageY - event.targetTouches[1].pageY));
if (hypo === undefined) {
hypo = hypo1;
}
callback(hypo1/hypo);
}
}, false);
el.addEventListener('touchend', function(event) {
hypo = undefined;
}, false);
}
The simplest way is to respond to the 'wheel' event.
You need to call ev.preventDefault() to prevent the browser from doing a full screen zoom.
Browsers synthesize the 'wheel' event for pinches on a trackpad, and as a bonus you also handle mouse wheel events. This is the way mapping applications handle it.
More details in my example:
let element = document.getElementById('el');
let scale = 1.0;
element.addEventListener('wheel', (ev) => {
// This is crucial. Without it, the browser will do a full page zoom
ev.preventDefault();
// This is an empirically determined heuristic.
// Unfortunately I don't know of any way to do this better.
// Typical deltaY values from a trackpad pinch are under 1.0
// Typical deltaY values from a mouse wheel are more than 100.
let isPinch = Math.abs(ev.deltaY) < 50;
if (isPinch) {
// This is a pinch on a trackpad
let factor = 1 - 0.01 * ev.deltaY;
scale *= factor;
element.innerText = `Pinch: scale is ${scale}`;
} else {
// This is a mouse wheel
let strength = 1.4;
let factor = ev.deltaY < 0 ? strength : 1.0 / strength;
scale *= factor;
element.innerText = `Mouse: scale is ${scale}`;
}
});
<div id='el' style='width:400px; height:300px; background:#ffa'>
Scale: 1.0
</div>
None of these answers achieved what I was looking for, so I wound up writing something myself. I wanted to pinch-zoom an image on my website using my MacBookPro trackpad. The following code (which requires jQuery) seems to work in Chrome and Edge, at least. Maybe this will be of use to someone else.
function setupImageEnlargement(el)
{
// "el" represents the image element, such as the results of document.getElementByd('image-id')
var img = $(el);
$(window, 'html', 'body').bind('scroll touchmove mousewheel', function(e)
{
//TODO: need to limit this to when the mouse is over the image in question
//TODO: behavior not the same in Safari and FF, but seems to work in Edge and Chrome
if (typeof e.originalEvent != 'undefined' && e.originalEvent != null
&& e.originalEvent.wheelDelta != 'undefined' && e.originalEvent.wheelDelta != null)
{
e.preventDefault();
e.stopPropagation();
console.log(e);
if (e.originalEvent.wheelDelta > 0)
{
// zooming
var newW = 1.1 * parseFloat(img.width());
var newH = 1.1 * parseFloat(img.height());
if (newW < el.naturalWidth && newH < el.naturalHeight)
{
// Go ahead and zoom the image
//console.log('zooming the image');
img.css(
{
"width": newW + 'px',
"height": newH + 'px',
"max-width": newW + 'px',
"max-height": newH + 'px'
});
}
else
{
// Make image as big as it gets
//console.log('making it as big as it gets');
img.css(
{
"width": el.naturalWidth + 'px',
"height": el.naturalHeight + 'px',
"max-width": el.naturalWidth + 'px',
"max-height": el.naturalHeight + 'px'
});
}
}
else if (e.originalEvent.wheelDelta < 0)
{
// shrinking
var newW = 0.9 * parseFloat(img.width());
var newH = 0.9 * parseFloat(img.height());
//TODO: I had added these data-attributes to the image onload.
// They represent the original width and height of the image on the screen.
// If your image is normally 100% width, you may need to change these values on resize.
var origW = parseFloat(img.attr('data-startwidth'));
var origH = parseFloat(img.attr('data-startheight'));
if (newW > origW && newH > origH)
{
// Go ahead and shrink the image
//console.log('shrinking the image');
img.css(
{
"width": newW + 'px',
"height": newH + 'px',
"max-width": newW + 'px',
"max-height": newH + 'px'
});
}
else
{
// Make image as small as it gets
//console.log('making it as small as it gets');
// This restores the image to its original size. You may want
//to do this differently, like by removing the css instead of defining it.
img.css(
{
"width": origW + 'px',
"height": origH + 'px',
"max-width": origW + 'px',
"max-height": origH + 'px'
});
}
}
}
});
}
My answer is inspired by Jeffrey's answer. Where that answer gives a more abstract solution, I try to provide more concrete steps on how to potentially implement it. This is simply a guide, one that can be implemented more elegantly. For a more detailed example check out this tutorial by MDN web docs.
HTML:
<div id="zoom_here">....</div>
JS
<script>
var dist1=0;
function start(ev) {
if (ev.targetTouches.length == 2) {//check if two fingers touched screen
dist1 = Math.hypot( //get rough estimate of distance between two fingers
ev.touches[0].pageX - ev.touches[1].pageX,
ev.touches[0].pageY - ev.touches[1].pageY);
}
}
function move(ev) {
if (ev.targetTouches.length == 2 && ev.changedTouches.length == 2) {
// Check if the two target touches are the same ones that started
var dist2 = Math.hypot(//get rough estimate of new distance between fingers
ev.touches[0].pageX - ev.touches[1].pageX,
ev.touches[0].pageY - ev.touches[1].pageY);
//alert(dist);
if(dist1>dist2) {//if fingers are closer now than when they first touched screen, they are pinching
alert('zoom out');
}
if(dist1<dist2) {//if fingers are further apart than when they first touched the screen, they are making the zoomin gesture
alert('zoom in');
}
}
}
document.getElementById ('zoom_here').addEventListener ('touchstart', start, false);
document.getElementById('zoom_here').addEventListener('touchmove', move, false);
</script>
Its same as commented by Jeffrey Sweeney, Full example to how to implement in your class.
this.touch.isPinch = false;
this.touc.pinchStart = 0;
this.touch.onTouchStart = (e) => {
if (e.touches.length === 2) {
this.touch.pinchStart = Math.hypot(e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY);
this.touch.isScaling = true;
}
}
this.touch.onTouchMove = (e) => {
if (this.touch.isScaling) {
const distance = Math.hypot(e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY);
if (this.touch.pinchStart >= 200 && distance <= 90) this.touchPichOut(); //call function for pinchOut
if (this.touch.pinchStart <= 100 && distance >= 280) this.touchPichIn(); //call function for pinchIn
}
}
this.touch.onTouchCancel = (e) => {
this.touch.isScaling = false;
}
this.touch.onTouchEnd = (e) => {
if (this.touch.isScaling) this.touch.isScaling = false;
}
Regards

Categories

Resources