JS Tooltip Positioning Flicker - javascript

Writing a pure JS tooltip library and have made it work really, really well. No big bells and whistles, just a super lightweight super utilitarian easy-to-customize tooltip. That said, I'm having an issue with updating the position; specifically when the tooltip is detected to be out of bounds.
And it works! It does. Issue is that when I'm hovered over the target element, which is against the right side of the page which causes the tooltip to be out of bounds, it will flicker between the default bottom-right position and the correct bottom-left position. Upon further inspection, it seems to only register out of bounds on every other hair I move the cursor. For instance, when I enter the element, it will display properly; move the cursor a hair in any direction and it will seemingly reset to bottom-right, not seeing that it's out of bounds. Move it one hair further and it will figure out it's out of bounds again, correcting itself to bottom-left. I'm not sure what assumptions to make here, but I'll let you guys be the judge of it.
updateTooltip(event) {
const mouseX = event.pageX,
mouseY = event.pageY;
const adjustment = 15;
let position = (this.element.getAttribute(this.selector + '-pos') || '').split(' ');
if (position.length !== 2) position = [ 'bottom', 'right' ];
var isOut = this.checkBounds();
if (isOut.top) { position[0] = 'bottom'; }
if (isOut.left) { position[1] = 'right'; }
if (isOut.bottom) { position[0] = 'top'; }
if (isOut.right) { position[1] = 'left'; }
let vertical, horizontal;
switch (position[0]) {
case 'top':
vertical = -adjustment - this.tooltip.offsetHeight;
break;
case 'center':
vertical = 0 - (this.tooltip.offsetHeight / 2);
break;
default:
vertical = adjustment;
}
switch (position[1]) {
case 'left':
horizontal = -adjustment - this.tooltip.offsetWidth;
break;
case 'center':
horizontal = 0 - (this.tooltip.offsetWidth / 2);
break;
default:
horizontal = adjustment;
}
this.tooltip.style.top = mouseY + vertical + 'px';
this.tooltip.style.left = mouseX + horizontal + 'px';
}
checkBounds() {
if (!this.tooltip) return;
const bounds = this.tooltip.getBoundingClientRect();
let out = {};
out.top = bounds.top < 0;
out.left = bounds.left < 0;
out.bottom = bounds.bottom > (window.innerHeight || document.documentElement.clientHeight);
out.right = bounds.right > (window.innerWidth || document.documentElement.clientWidth);
out.any = out.top || out.left || out.bottom || out.right;
out.all = out.top && out.left && out.bottom && out.right;
return out;
}

Related

Js collision detector - hit the wall vs. hit the floor (difference)

I'm making a small game with one player and blocks that builds up the environment. The problem I'm having is knowing the difference between when the player hits the ground (the top of a block), and hitting a wall (the side of the block).
So far the player can walk on the ground just fine, but when he meets a wall, he immediately jumps to the top of that block.
This is my collision detector:
function collisionDetector(){
if(myPlayer.y + myPlayer.h > c.height){ //Bottom of the canvas
myPlayer.vy = 0;
myPlayer.ay = 0;
myPlayer.y = c.height - myPlayer.h;
myPlayer.onGround = true;
console.log(myPlayer.y + myPlayer.h, c.height);
}
if(myPlayer.x + myPlayer.w >= c.width){ //right side of canvas
myPlayer.x = c.width - myPlayer.w;
myPlayer.vx = 0;
}
if(myPlayer.x <= 0){ //Left side of canvas
myPlayer.x = 0;
myPlayer.vx = 0;
}
function hitTest(a,b){ //hitTest between two objects
if(a.y + a.h > b.y && a.y < b.y + b.h && a.x + a.w > b.x && a.x < b.x + b.w){
return true;
}
}
for(var i = 0; i < blocks.length; i++){ //Loop through blocks
if(hitTest(myPlayer, blocks[i])){ //If it touches a block
myPlayer.y = blocks[i].y - myPlayer.h;
myPlayer.onGround = true; //onGround = ready to jump
}
}
}
I realized that I'm setting the players y pos to be on top of what ever block it hits, but I cannot figure out a solution to this problem. Can anyone help me or at least lead me in the right direction? Thanks!
(Let me know if you need more of the code)
PS: the player is just a head. No body hiding behind the blocks.
So basically, what you need to do is to check collision between many points in the player.
In the snippet you can show many points represented in the player.
Bottom almost-left and almost-right (in blue), check against below blocks. They are not fully left or right, in order to prevent a race condition which will allow the player to climb walls. In that case, if the player is pushing against a wall and jumping, the collider will detect both side collision and bottom collision as true, then the player will quickly move to the top until there are no more blocks.
Left and right points (in black), check against edges of the blocks. This is just a point instead of two like the bottom edge, because we don't need more for this particular case. One more for each side could be easily added to get a better detection.
Top point (in red) checks against the top blocks. This is in the middle in order to allow the player a more easy way to tranverse the map. If this is not needed, you would need to add one more point like in the bottom edge (but never reaching the far edge, because that will generate a race condition).
So in summary, to have a good collision detection based on points (instead of raycasts), you need to detect the player like if it where a rounded shape, in order to prevent strange behaviours.
You can player around with the map layout by altering the layout variable. 0's are empty space, 1's are brown blocks and 2's are green blocks.
The collisionDetector fuction has comments to understand what's going on.
Also I have added a jump feature since I understand you would need that as well.
const c = document.getElementById('canvas');
c.width = window.innerWidth;
c.height = window.innerHeight;
const ctx = c.getContext('2d');
// map layout
const layout =
`000000001
001000001
000000101
100110111
222222222`;
// convert layout to blocks
const blocks = [...layout].reduce((a, c, i) => {
if (i === 0 || c === "\n") a.push([]);
if (c === "\n") return a;
const y = a.length - 1;
const row = a[y];
const x = row.length;
row.push({x: x * 32, y: y * 32, t:c, w:32, h:32});
return a;
}, []).reduce((a, c) => a.concat(c), []);
// player starting position
const myPlayer = {x: 32*1.5, y: 0, h: 32, w: 16, onGround: true};
const gravity = -1;
let pkl = 0, pkr = 0;
let pvely = 0;
function render() {
// player logic
const pvelx = pkr + pkl;
const speed = 2;
myPlayer.x += pvelx * speed;
myPlayer.y -= pvely;
if (pvely > -2) pvely += gravity;
const debugColliders = collisionDetector();
ctx.clearRect(0, 0, c.width, c.height);
// player render
ctx.fillStyle = '#FFD9B3';
ctx.fillRect(myPlayer.x, myPlayer.y, myPlayer.w, myPlayer.h);
renderLayout();
debugColliders();
window.requestAnimationFrame(render);
}
function renderLayout() {
const colors = {'1': '#A3825F', '2': '#7FAC72'}
blocks.forEach(b => {
if (+b.t > 0) {
ctx.fillStyle = colors[b.t];
ctx.fillRect(b.x, b.y, b.w, b.h);
}
});
}
window.addEventListener('keydown', e => {
if (e.key == 'ArrowRight') {
pkr = 1;
e.preventDefault();
} else if (e.key == 'ArrowLeft') {
pkl = -1;
e.preventDefault();
} else if (e.key == 'ArrowUp') {
if (myPlayer.onGround)
pvely = 8;
myPlayer.onGround = false;
e.preventDefault();
}
});
window.addEventListener('keyup', e => {
if (e.key == 'ArrowRight') {
pkr = 0;
} else if (e.key == 'ArrowLeft') {
pkl = 0;
}
});
function collisionDetector(){
const p = myPlayer;
const playerTop = p.y;
const playerLeft = p.x;
const playerRight = playerLeft + p.w;
const playerBottom = playerTop + p.h;
const playerHalfLeft = playerLeft + p.w * .25;
const playerHalfRight = playerLeft + p.w * .75;
const playerHMiddle = playerLeft + p.w * .5;
const playerVMiddle = playerTop + p.h * .5;
if(playerBottom > c.height){ //Bottom of the canvas
p.vy = 0;
p.ay = 0;
p.y = c.height - p.h;
p.onGround = true;
}
if(playerRight >= c.width){ //right side of canvas
p.x = c.width - p.w;
p.vx = 0;
}
if(playerLeft <= 0){ //Left side of canvas
p.x = 0;
p.vx = 0;
}
blocks.forEach(b => { //Loop through blocks
if (b.t === "0") return; // If not collidable, do nothing
const blockTop = b.y;
const blockLeft = b.x;
const blockRight = blockLeft + b.w;
const blockBottom = b.y + b.h;
// Player bottom against block top
if ((playerBottom > blockTop && playerBottom < blockBottom) && // If player bottom is going through block top but is above block bottom.
((playerHalfLeft > blockLeft && playerHalfLeft < blockRight) || // If player left is inside block horizontal bounds
(playerHalfRight > blockLeft && playerHalfRight < blockRight))) { // Or if player right is inside block horizontal bounds
p.y = blockTop - p.h;
p.onGround = true;
}
// Player top against block bottom
if ((playerTop < blockBottom && playerTop > blockTop) && // If player top is going through block bottom but is below block top.
((playerHMiddle > blockLeft && playerHMiddle < blockRight))) { // If player hmiddle is inside block horizontal bounds
p.y = blockBottom;
p.onGround = false;
}
// Player right against block left, or player left against block right
if (playerVMiddle > blockTop && playerVMiddle < blockBottom) { // If player vertical-middle is inside block vertical bounds
if ((playerRight > blockLeft && playerRight < blockRight)) { // If player vmiddle-right goes through block-left
p.x = blockLeft - p.w;
} else if ((playerLeft < blockRight && playerRight > blockLeft)) { // If player vmiddle-left goes through block-right
p.x = blockRight;
}
}
});
return function debug() {
ctx.fillStyle = 'black';
ctx.fillRect(playerLeft, playerVMiddle, 1, 1);
ctx.fillRect(playerRight, playerVMiddle, 1, 1);
ctx.fillStyle = 'red';
ctx.fillRect(playerHMiddle, playerTop, 1, 1);
ctx.fillStyle = 'blue';
ctx.fillRect(playerHalfLeft, playerBottom, 1, 1);
ctx.fillRect(playerHalfRight, playerBottom, 1, 1);
}
}
window.requestAnimationFrame(render);
html, body{ width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
canvas { background: #7AC9F9; display: block; }
<canvas id="canvas"></canvas>
Introduce a block[i].type attribute. For instance if block[i].type=='floor' then make player stay on floor. If for another instance block[i].type=='wall' then make it stop moving through the wall. When block[i].type=='brick' or square or block or whatever, them it's a mixture of two.
Another part to be edited is when you check the collisions. What if you have only one-direction collision? What I am saying is maybe use or instead of and in this part if(a.y + a.h > b.y && a.y < b.y + b.h && a.x + a.w > b.x && a.x < b.x + b.w){
Also you could check each collision separately, like
function hitTest(a,b){ //hitTest between two objects
var collisions = {up: false, down: false, left: false, right: false};
collisions.up = (a.y + a.h > b.y ) || collisions.up
collisions.down = (a.y < b.y + b.h ) ||collisions.down
collisions.right = ( a.x + a.w > b.x) || collisions.right
collisions.left = (a.x < b.x + b.w) || collisions.left
return collisions
}
var escapeFrom = {
down: function(player, block){
player.y = block.y + block.h;
player.onGround = true; //onGround = ready to jump
},
up: function(player, block){
// you logic to escape from hitting the ceiling
},
// and for the next 2
left: function(player, block) {},
right: function(player, block){}
}
// Now here you check whether your player hits blocks
for(var i = 0; i < blocks.length; i++){ //Loop through blocks
cls = hitTest(myPlayer, blocks[i]) //If it touches a block
Object.keys(cls).map(function(direction, ind){
if (cls[direction]){
// call escape from function to escape collision
escapeFrom[direction](myPlayer, blocks[i]);
}
})
}
This is highly unoptimized, the whole your code is unoptimized, but at least it can help to move further.

convert function to use css translate3d

I've written a function in Javascript to make images draggable within a container. Even if the image is enlarged it can be dragged all over the screen without disappearing from it. My function relies heaving on using style.top and style.left. Now I've heard that using translate3d might provide better performance. This is interesting because I changed my image scale function, which uses a slider, to scale3d and the scaling is clearly smoother, no doubt. So could anyone help me convert this function I've written to use translate3d? I've tried and tried but have kept failing. Many Thanks:
EDIT: I put up a jsfiddle
https://jsfiddle.net/bx4073tr/
Please note that imgRect is the parent div while img is the image itself (it's in an img tag contained in the div).
function makeImageDraggable(event) {
// Make an image draggable but within bounds of container
let overflow_vertical = false;
let overflow_horizontal = false;
// bounding rectangles to hold image and imageContainer
let imgRect = img.getBoundingClientRect();
let imgContainerRect = imageContainer.getBoundingClientRect();
// find out if image overflows it's container div
// check for vertical overflow, getBoundingClientRect().height will get the real height after the image is scaled
if ( imgRect.height > imageContainer.offsetHeight ) {
overflow_vertical = true;
}
// check for horizontal overflow
if ( imgRect.width > imageContainer.offsetWidth ) {
overflow_horizontal = true;
}
// if there is no overflow, either horizontal or vertical, then do absolutely nothing
if (!overflow_horizontal && !overflow_vertical) {
// nothing to do
} else {
// otherwise make image draggable
event = event || window.event;
// get initial mouse position
let startX = event.clientX;
let startY = event.clientY;
// get position of image to be dragged
let offsetX = pixelToFloat(img.style.left);
let offsetY = pixelToFloat(img.style.top);
// add onmousemove event now we are sure user has initiated a mousedown event
window.onmousemove = function(mousemove_event) {
if (mousemove_event == null) {
mousemove_event = window.event;
}
// calculate bounds so that image does not go off the page
// if there is an overflow, the image will be bigger than the container
// so we need to find the maximum distance we can go upwards, downwards and sideways
// using img.getBoundingClientRect, we can get the width of the scaled image, we also get the width of the container
// divide it by 2 so we can move the same number of pixels in either direction
// max right and left
let max_right = -1 * ( ((imgRect.right - imgRect.left) - (imgContainerRect.right - imgContainerRect.left))/2 );
// should be a positive number
let max_left = -1 * (max_right);
// max bottom and top
let max_bottom = -1 * ( ((imgRect.bottom - imgRect.top) - (imgContainerRect.bottom - imgContainerRect.top))/2 );
// should be a positive number
let max_top = -1 * (max_bottom);
// Dragging image left and right
if (!overflow_horizontal) {
} else {
let scrollX = (offsetX + mousemove_event.clientX - startX);
// img.style.left will keep increasing or decreasing, check if it approaches max_left or max_right
if (scrollX >= max_left || scrollX <= max_right) {
//return false;imageContainer.style.webkitTransform = 'translate3d(' + newX + 'px,' + newY + 'px, 0)';
} else {
if (scrollX < max_left) { img.style.left = min(scrollX, max_left) + 'px'; }
if (scrollX > max_right) { img.style.left = max(scrollX, max_right) + 'px'; }
}
}
// Dragging image top to bottom
if (!overflow_vertical) {
} else {
let scrollY = (offsetY + mousemove_event.clientY - startY);
// as an expanded image is pulled downwards, img.style.top keeps increasing to approach max_top
// if it reaches max top, simply do nothing, else keep increasing
// check for both conditions, approaching max_top and approaching max_bottom
if (scrollY >= max_top || scrollY <= max_bottom) {
// return false;
} else {
if (scrollY < max_top) { img.style.top = min(scrollY, max_top) + 'px'; }
if (scrollY > max_bottom) { img.style.top = max(scrollY, max_bottom) + 'px'; }
}
}
// return
return false;
}
}
// cancel mousemove event on mouseup
window.onmouseup = function(mouseup_event) {
window.onmousemove = null;
// Should not return false as it will interfere with range slider
}
// return false
return false;
}
Works now.
See makeDraggable method in the fiddle below:
https://jsfiddle.net/daibatzu/0u74faz6/6/
All you have to do is add this function to the event listener for the image like:
var img = document.getElementById('myImage');
img.addEventListener('mousedown', function(event) { makeDraggable(event); });
Code
function makeDraggable(event) {
// get bounding rectangle
let imgRect = img.getBoundingClientRect();
let parentRect = parent.getBoundingClientRect();
// check overflow
let overflow_horizontal = (imgRect.width > parent.offsetWidth ? true : false);
let overflow_vertical = (imgRect.height > parent.offsetHeight ? true : false);
// get start position
let startX = event.pageX - translateX, startY = event.pageY - translateY;
let max_left = parentRect.left - imgRect.left;
let max_top = parentRect.top - imgRect.top;
window.onmousemove = function(evt) {
// set event object
if (evt == null) { evt = window.event; }
// Say max_left is 160px, this means we can only translate from 160px to -160px to keep the image visible
// so we check if the image moves beyond abs(160), if it does, set it to 160 or -160 depending on direction, else, let it continue
translateX = (Math.abs(evt.pageX - startX) >= max_left ? (max_left * Math.sign(evt.pageX - startX)) : (evt.pageX - startX));
translateY = (Math.abs(evt.pageY - startY) >= max_top ? (max_top * Math.sign(evt.pageY - startY)) : (evt.pageY - startY));
// check if scaled image width is greater than it's container. if it isn't set translateX to zero and so on
translateX = overflow_horizontal ? translateX : 0, translateY = overflow_vertical ? translateY : 0;
// translate parent div
parent.style['-webkit-transform'] = 'translate(' + translateX + 'px, ' + translateY + 'px)';
// return
return false;
}
window.onmouseup = function(evt) {
// set mousemove event to null
window.onmousemove = null;
}
return false;
};

Why do my laser rays flicker?

I'm trying my hands at some simple game programming in Javascript and have come to realize I need to change the way I handle sprites. The only question is, "how"?
I have a hero that moves around with the arrow keys and fires laser rays with WASD. This is how I define rays:
function Ray (x, y, width, height, direction, index) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.direction = direction;
this.index = index;
this.speed = 512;
this.disabled = false;
}
The index just indicates where in an array of rays (heh) it is being stored. I currently have a hard-coded limit of 5 simultaneous rays, although the other restrictions (screen size, ray size, speed, hero size etc) shouldn't allow for more than 4:
var rays = [];
var numberOfRays = 0;
var rayLimit = 5;
var shotClock = 300;
And so, in the update() function that gets called by the game loop, I have listeners for the WASD keys. They look like this:
// D
if (68 in keysDown && numberOfRays <= rayLimit && Date.now() - lastShootTime > shotClock) {
lastShootTime = Date.now();
var newRayIndex = findFreeRay();
rays[newRayIndex] = new Ray(hero.x + hero.width + 12, hero.y + hero.height / 2, rayImage.width, rayImage.height, 'right', newRayIndex);
numberOfRays++;
}
(findFreeRay() just returns the lowest unused or disabled (off the screen) index in rays[])
Earlier in the update() method (I have also tried putting it later) I have the logic for updating ray movement:
rays.forEach(function(ray) {
if (ray != null && !ray.disabled) {
switch(ray.direction) {
case 'right':
ray.x += ray.speed * modifier;
break;
case 'left':
ray.x -= ray.speed * modifier;
break;
case 'up':
ray.y -= ray.speed * modifier;
break;
case 'down':
ray.y += ray.speed * modifier;
break;
}
}
});
Finally, there is the image for the ray (actually, one for horizontal rays and another one for vertical rays). Currently, I am using one Image object of each globally, that the existing rays share. But I have also tried, without much luck, to create individual image objects for every ray.
// Ray images
var rayReady = false;
var rayImage = new Image();
rayImage.onload = function() {
rayReady = true;
};
rayImage.src = "images/ray.png";
var rayVertReady = false;
var rayVertImage = new Image();
rayVertImage.onload = function() {
rayVertReady = true;
};
rayVertImage.src = "images/ray_vert.png";
And here is how they get drawn:
if (rayReady && rayVertReady && numberOfRays > 0) {
rays.forEach(function(ray) {
if (ray.x > canvas.width
|| ray.x + ray.width < 0
|| ray.y > canvas.height
|| ray.y + ray.height < 0) {
numberOfRays--;
ray.disabled = true;
}
else if (ray.direction == 'right' || ray.direction == 'left'){
ctx.drawImage(rayImage, ray.x, ray.y);
}
else {
ctx.drawImage(rayVertImage, ray.x, ray.y);
}
});
}
The problem
After shooting only a few rays, new ones start to either flicker and disappear, or stay invisible altogether. They actually exist as gameplay objects though, as they can hit targets.
What likely causes this flickering?
(credit to Matt Hackett for the base of this code)
fiddle: http://jsfiddle.net/Vr3MW/

Bounds on image resize in HTML5 Canvas

I am trying to add size limits on image resize using KineticJS library. HTML5 Canvas Tutorial provides the method to resize the image: http://www.html5canvastutorials.com/labs/html5-canvas-drag-and-drop-resize-and-invert-images/
But I want the user not to be able to resize the image to less than 50px x 50px or greater than 200 x 200.
I have tried a lot manage the controls by following code. But it shows very strange results
dragBoundFunc: function(pos) {
var newX = '';
var image_width = group.get(".darthVaderImg")[0].getWidth();
var image_height = group.get(".darthVaderImg")[0].getHeight();
var image_position = group.get(".darthVaderImg")[0].getPosition();
if((image_width>50 && image_width< 200) ){
newX = pos.x;
}else{
newX = image_position.x+image_width+80;
}
if((image_height>50 && image_height< 200) ){
newY = pos.y;
}else{
newY = image_position.y+100;
}
return {
x: newX ,
y: newY,
};
}
Any example or idea that how I can do it
http://jsfiddle.net/e4uyG/4/ gets you pretty close to what you are asking for.
Basically, you have an update() function which redraws the images, so just limit it to a certain amount of resizing
function update(group, activeAnchor) {
var topLeft = group.get(".topLeft")[0];
var topRight = group.get(".topRight")[0];
var bottomRight = group.get(".bottomRight")[0];
var bottomLeft = group.get(".bottomLeft")[0];
var image = group.get(".image")[0];
// update anchor positions
switch (activeAnchor.getName()) {
case "topLeft":
topRight.attrs.y = activeAnchor.attrs.y;
bottomLeft.attrs.x = activeAnchor.attrs.x;
break;
case "topRight":
topLeft.attrs.y = activeAnchor.attrs.y;
bottomRight.attrs.x = activeAnchor.attrs.x;
break;
case "bottomRight":
bottomLeft.attrs.y = activeAnchor.attrs.y;
topRight.attrs.x = activeAnchor.attrs.x;
break;
case "bottomLeft":
bottomRight.attrs.y = activeAnchor.attrs.y;
topLeft.attrs.x = activeAnchor.attrs.x;
break;
}
image.setPosition(topLeft.attrs.x, topLeft.attrs.y);
var width = topRight.attrs.x - topLeft.attrs.x;
var height = bottomLeft.attrs.y - topLeft.attrs.y;
if(height > 50 && height < 200) // Limit the height
image.setHeight(height);
if(width > 50 && width < 200) // Limit the width
image.setWidth(width);
}

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