I am trying to build an interface that allows both resize/drag and rotate on some element and to achieve this I am using interact.js javascript library.
I have my interact functions working:
interact('.resize-drag-ratio')
.draggable({
onmove: window.dragMoveListener
})
.resizable({
preserveAspectRatio: true,
edges: { left: true, right: true, bottom: true, top: true }
})
.on('resizemove', function (event) {
var target = event.target,
x = (parseFloat(target.getAttribute('data-x')) || 0),
y = (parseFloat(target.getAttribute('data-y')) || 0);
var min_size = 35;
if(event.rect.width>min_size){
// update the element's style
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.style.webkitTransform = target.style.transform =
'translate(' + x + 'px,' + y + 'px)';
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
});
and drag-rotate that allows for rotation
interact('.drag-rotate')
.draggable({
onstart: function (event) {
const element = event.target;
const rect = element.getBoundingClientRect();
// store the center as the element has css `transform-origin: center center`
element.dataset.centerX = rect.left + rect.width / 2;
element.dataset.centerY = rect.top + rect.height / 2;
console.log("element.dataset.centerX: "+element.dataset.centerX);
console.log("element.dataset.centerY: "+element.dataset.centerY);
// get the angle of the element when the drag starts
element.dataset.angle = getDragAngle(event);
},
onmove: function (event) {
var element = event.target;
var center = {
x: 300,
y: 300,
};
console.log("element.dataset.centerX: "+element.dataset.centerX);
console.log("element.dataset.centerY: "+element.dataset.centerY);
var angle = getDragAngle(event);
// update transform style on dragmove
element.style.transform = 'rotate(' + angle + 'rad' + ')';
},
onend: function (event) {
const element = event.target;
// save the angle on dragend
element.dataset.angle = getDragAngle(event);
},
})
The two classes get switched using jQuery thus turning drag into rotation and vice versa.
My problem is that object location and rotation angle do not stay as placed and I am not sure how to fix that.
After i drag an element to some position and press the rotate button, one I start rotating the element it moves to top:0px left:0px and does not stay at its dragged position.
you can see the full working code right here:
https://codepen.io/yaary-vidanpeled/pen/ZZwGmE
The Issue:
This is happening because each time you apply css, you are overwriting your previous styles.
Here is an example, let's say you have a text ( #text element ) with color of red, now you want to change it with JavaScript.
document.getElementById('text').style.color = 'green';
what actually happened here ? whatever was assigned as the color property of the style object is now overwritten.
The same thing is happening when you are writing (in your interact initialisation of .resize-drag-ratio):
target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
And overwriting the translate again by writing (in your interact initialisation of .drag-rotate)
element.style.transform = 'rotate(' + angle + 'rad' + ')';
Remember that the rotate() and translate() both are values of the css translate property.
The Solution:
You should somehow preserve the all these rotation angle, and translate values. (Looks like you already have data-attribtues for them so it wont be hard)
And apply the value of the element.style.transform as following:
target.style.transform = 'translate(' + x + 'px,' + y + 'px) rotate(' + angle + 'rad)';
Note: Your snippet has the function dragMoveListener(event) { declared twice.
Working Snippet:
console.log('start');
//function isEven
function isEven(n) {
return n == parseFloat(n) ? !(n % 2) : void 0;
}
interact('.resize-drag-ratio')
.draggable({
onmove: window.dragMoveListener
})
.resizable({
preserveAspectRatio: true,
edges: {
left: true,
right: true,
bottom: true,
top: true
}
})
.on('resizemove', function(event) {
var target = event.target,
x = (parseFloat(target.getAttribute('data-x')) || 0),
y = (parseFloat(target.getAttribute('data-y')) || 0);
rotation = (parseFloat(target.getAttribute('data-angle')) || 0)
var min_size = 35;
if (event.rect.width > min_size) {
// update the element's style
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.style.webkitTransform = target.style.transform =
'translate(' + x + 'px,' + y + 'px) rotate(' + rotation + 'rad)';
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
});
interact('.resize-drag')
.draggable({
onmove: window.dragMoveListener
})
.resizable({
preserveAspectRatio: false,
edges: {
left: true,
right: true,
bottom: true,
top: true
}
})
.on('resizemove', function(event) {
var target = event.target,
x = (parseFloat(target.getAttribute('data-x')) || 0),
y = (parseFloat(target.getAttribute('data-y')) || 0),
rotation = (parseFloat(target.getAttribute('data-angle')) || 0)
//console.log("event.rect.width: "+event.rect.width);
//prevents resizing to units smaller then 35px
var min_size = 35;
if (event.rect.width > min_size) {
// update the element's style
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
//$("#form_bubble_width").val(event.rect.width);
//$("#form_bubble_width").val(event.rect.height);
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.style.webkitTransform = target.style.transform =
'translate(' + x + 'px,' + y + 'px) rotate(' + rotation + 'rad)';
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
//target.textContent = event.rect.width + '×' + event.rect.height;
}
});
// target elements with the "draggable" class
interact('.draggable')
.draggable({
// enable inertial throwing
inertia: true,
// keep the element within the area of it's parent
restrict: {
restriction: "parent",
endOnly: true,
elementRect: {
top: 0,
left: 0,
bottom: 1,
right: 1
}
},
// enable autoScroll
autoScroll: true,
// call this function on every dragmove event
onmove: dragMoveListener,
// call this function on every dragend event
onend: function(event) {
// var textEl = event.target.querySelector('p');
console.log(event.target.id)
var distance = (Math.sqrt(Math.pow(event.pageX - event.x0, 2) +
Math.pow(event.pageY - event.y0, 2) | 0))
.toFixed(2) + 'px';
}
});
interact('.resize-drag , .resize-drag-ratio').on('tap', function(event) {
event.preventDefault();
var target = event.target
console.log("tap resize-drag class element");
var uuid = target.id;
//console.log("uuid: "+uuid);
console.log("click");
});
function dragMoveListener(event) {
var target = event.target,
// keep the dragged position in the data-x/data-y attributes
x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy,
rotation = (parseFloat(target.getAttribute('data-angle')) || 0);
// translate the element
target.style.webkitTransform =
target.style.transform =
'translate(' + x + 'px, ' + y + 'px) rotate(' + rotation + 'rad)';
// update the posiion attributes
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
target.setAttribute('data-angle', rotation);
}
// this is used later in the resizing and gesture demos
window.dragMoveListener = dragMoveListener;
var mouseX = 0,
mouseY = 0
//function onMousemove(e)
function onMousemove(e) {
var m_posx = 0,
m_posy = 0,
e_posx = 0,
e_posy = 0,
obj = this;
//get mouse position on document crossbrowser
if (!e) {
e = window.event;
}
if (e.pageX || e.pageY) {
m_posx = e.pageX;
m_posy = e.pageY;
} else if (e.clientX || e.clientY) {
m_posx = e.clientX + document.body.scrollLeft +
document.documentElement.scrollLeft;
m_posy = e.clientY + document.body.scrollTop +
document.documentElement.scrollTop;
}
//get parent element position in document
if (obj.offsetParent) {
do {
e_posx += obj.offsetLeft;
e_posy += obj.offsetTop;
} while (obj = obj.offsetParent);
}
// mouse position minus elm position is mouseposition relative to element:
dbg.innerHTML = ' X Position: ' + (m_posx - e_posx) +
' Y Position: ' + (m_posy - e_posy);
mouseX = (m_posx - e_posx);
mouseY = (m_posy - e_posy);
}
var elem = document.getElementById('container');
//elem.addEventListener('mousemove', onMousemove, false);
var dbg = document.getElementById('dbg'); //just for debug div instead of console
$(document).ready(function() {
var is_rotate = true;
$("#btn_rotate").click(function() {
// console.log('ddd');
if (is_rotate) {
$(this).text('drag-resize');
$(".element").removeClass("drag-rotate");
$(".element").addClass("resize-drag-ratio");
is_rotate = false;
} else {
$(this).text('rotate');
$(".element").removeClass("resize-drag-ratio");
$(".element").addClass("drag-rotate");
is_rotate = true;
}
//console.log('click: '+is_rotate);
});
var saved_mouseX = 0;
var saved_mouseY = 0;
//interact("#container").on('tap', function (event) {
interact("#container").on('tap', function(event) {
event.preventDefault();
var target = event.target
if (target.id == "tp_image") {
console.log(target.id);
console.log(mouseX + "-" + mouseY);
saved_mouseX = mouseX;
saved_mouseY = mouseY;
//$('#modal_stickers').modal('show');
}
});
//interact('.drag-rotate')
interact('.drag-rotate')
.draggable({
onstart: function(event) {
const element = event.target;
const rect = element.getBoundingClientRect();
// store the center as the element has css `transform-origin: center center`
element.dataset.centerX = rect.left + rect.width / 2;
element.dataset.centerY = rect.top + rect.height / 2;
// console.log("element.dataset.centerX: " + element.dataset.centerX);
// console.log("element.dataset.centerY: " + element.dataset.centerY);
// get the angle of the element when the drag starts
element.dataset.angle = getDragAngle(event);
},
onmove: function(event) {
var element = event.target;
var center = {
x: 300,
y: 300,
};
// console.log("element.dataset.centerX: " + element.dataset.centerX);
// console.log("element.dataset.centerY: " + element.dataset.centerY);
var angle = getDragAngle(event);
var x = element.dataset.x;
var y = element.dataset.y;
// update transform style on dragmove
// this is where the bug was; at initial point, there was no x, or y position set on the dataset of the element. thus your style value would be undefined, so here we check the values of x and y first and set the style accordingly;
if (typeof x != 'undefined' && typeof y != 'undefined') {
element.style.transform = 'translate(' + x + 'px, ' + y + 'px) rotate(' + angle + 'rad' + ')';
} else {
element.style.transform = 'rotate(' + angle + 'rad' + ')';
}
},
onend: function(event) {
const element = event.target;
// save the angle on dragend
element.dataset.angle = getDragAngle(event);
},
})
//function getDragAngle(event)
function getDragAngle(event) {
var element = event.target;
var startAngle = parseFloat(element.dataset.angle) || 0;
var center = {
x: parseFloat(element.dataset.centerX) || 0,
y: parseFloat(element.dataset.centerY) || 0,
};
var angle = Math.atan2(center.y - event.clientY,
center.x - event.clientX);
return angle - startAngle;
}
});
#btn_rotate {
position: absolute;
top: 0;
left: 0;
cursor: pointer;
background: #ccc;
padding: 30px;
}
.element {
width: 25%;
min-height: 6.5em;
margin: 10%;
background-color: #29e;
color: white;
/* added later */
touch-action: none;
box-sizing: border-box;
}
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.0.js"></script>
<script type="text/javascript" src="https://unpkg.com/interactjs#next/dist/interact.js"></script>
<div class="element drag-rotate">
<p> drag to rotate</p>
</div>
<div id="btn_rotate">rotate
</div>
Check line 288 on the script part. there is a comment explaining about the if block and why this was happening.
Related
I've both tried to solve the problem as well as search StackOverflow for multiple solutions, none that seemed to work properly. I have what appears to be a close but not quite the end result I'm looking for. I'm trying to make it so when the user zooms using the mousewheel, the zoom being based on the cursor's position.
Could someone explain what I'm doing wrong here. Somewhere in my calculation for the image offset im doing something wrong which you'll see when you test it.
// offset image based on cursor position
var width = img_ele.getBoundingClientRect().width;
var height = img_ele.getBoundingClientRect().height;
var x_cursor = window.event.clientX;
var y_cursor = window.event.clientY;
var x = img_ele.offsetLeft;
var y = img_ele.offsetTop;
// Calculate displacement of zooming position.
var dx = x - ((x_cursor - x) * (factor - 1.0));
var dy = y - ((y_cursor - y) * (factor - 1.0));
img_ele.style.left = dx + 'px';
img_ele.style.top = dy + 'px';
Below is the full code. Just change the src image to one of your liking.
<!DOCTYPE html>
<html>
<body>
<div id="container">
<img ondragstart="return false" id="drag-img" src="map.jpg" />
</div>
<input type="button" id="zoomout" class="button" value="Zoom out">
<input type="button" id="zoomin" class="button" value="Zoom in">
<input type="button" id="zoomfit" class="button" value="Zoom fit">
</body>
</html>
<script>
var img_ele = null,
x_cursor = 0,
y_cursor = 0,
x_img_ele = 0,
y_img_ele = 0;
function zoom(factor) {
img_ele = document.getElementById('drag-img');
// Zoom into the image.
var width = img_ele.getBoundingClientRect().width;
var height = img_ele.getBoundingClientRect().height;
img_ele.style.width = (width * factor) + 'px';
img_ele.style.height = (height * factor) + 'px';
// offset image based on cursor position
var width = img_ele.getBoundingClientRect().width;
var height = img_ele.getBoundingClientRect().height;
var x_cursor = window.event.clientX;
var y_cursor = window.event.clientY;
var x = img_ele.offsetLeft;
var y = img_ele.offsetTop;
// Calculate displacement of zooming position.
var dx = x - ((x_cursor - x) * (factor - 1.0));
var dy = y - ((y_cursor - y) * (factor - 1.0));
img_ele.style.left = dx + 'px';
img_ele.style.top = dy + 'px';
console.log('MAP SIZE : ' + width, height);
console.log('MAP TOP/LEFT : ' + x, y, 'NEW:', dx, dy);
console.log('CURSOR : ' + x_cursor, y_cursor);
img_ele = null;
}
function zoom_fit() {
bb_el = document.getElementById('container')
img_el = document.getElementById('drag-img');
var width = bb_el.getBoundingClientRect().width;
var height = bb_el.getBoundingClientRect().height;
img_el.style.width = width + 'px';
img_el.style.height = height + 'px';
img_el.style.left = '0px';
img_el.style.top = '0px';
img_el = null;
}
document.getElementById('zoomout').addEventListener('click', function() {
zoom(0.9);
});
document.getElementById('zoomin').addEventListener('click', function() {
zoom(1.1);
});
document.getElementById('zoomfit').addEventListener('click', function() {
zoom_fit();
});
document.addEventListener('mousewheel', function(event) {
event.preventDefault();
x_cursor = window.event.clientX;
y_cursor = window.event.clientY;
// console.log('ZOOM : ' + 'X:', x_cursor, 'Y:', y_cursor);
if (event.deltaY < 0) {
// console.log('scrolling up');
zoom(1.1);
}
if (event.deltaY > 0) {
// console.log('scrolling down');
zoom(0.9);
}
});
function start_drag() {
img_ele = this;
// console.log('inside start_drag var img_ele set to : ' + this);
x_img_ele = window.event.clientX - document.getElementById('drag-img').offsetLeft;
y_img_ele = window.event.clientY - document.getElementById('drag-img').offsetTop;
// console.log('start_drag : ' + 'x_img_ele:', x_img_ele, 'y_img_ele:', y_img_ele);
}
function stop_drag() {
// console.log('stop drag');
img_ele = null;
}
function while_drag() {
// console.log('while_drag: ', window.event);
var x_cursor = window.event.clientX;
var y_cursor = window.event.clientY;
if (img_ele !== null) {
img_ele.style.left = (x_cursor - x_img_ele) + 'px';
img_ele.style.top = ( window.event.clientY - y_img_ele) + 'px';
// console.log('while_drag: ','x_cursor', x_cursor, 'x_img_ele', x_img_ele, 'y_cursor', y_cursor, 'y_img_ele', y_img_ele,'Image left and top', img_ele.style.left+' - '+img_ele.style.top);
}
}
document.getElementById('drag-img').addEventListener('mousedown', start_drag);
document.getElementById('container').addEventListener('mousemove', while_drag);
document.getElementById('container').addEventListener('mouseup', stop_drag);
function while_drag() {
var x_cursor = window.event.clientX;
var y_cursor = window.event.clientY;
if (img_ele !== null) {
img_ele.style.left = (x_cursor - x_img_ele) + 'px';
img_ele.style.top = ( window.event.clientY - y_img_ele) + 'px';
// console.log(img_ele.style.left+' - '+img_ele.style.top);
}
}
document.getElementById('drag-img').addEventListener('mousedown', start_drag);
document.getElementById('container').addEventListener('mousemove', while_drag);
document.getElementById('container').addEventListener('mouseup', stop_drag);
</script>
<style>
#drag-img {
cursor: move;
position: relative;
width: 400px;
height: 400px;
}
#container {
overflow: hidden;
background: red;
width: 400px;
height: 400px;
}
.button {
width: 80px;
height: 25px;
}
</style>
if (action == "mousedown") {
startx = x;
starty = y;
}
if (action == "mousemove") {
if (!mouseisdown) {
return;
} else {
//console.log(target);
var transformX = x - startx;
var transformY = y - starty;
// console.log(x, y, startx, starty);
var transformAttr = 'translate(' + transformX + ',' + transformY + ')';
test.setAttribute('transform', transformAttr);
}
}
if (action == "mouseup") {
var transformX = x - startx;
var transformY = y - starty;
// console.log(x, y, startx, starty);
var transformAttr = 'translate(' + transformX + ',' + transformY + ')';
test.setAttribute('transform', transformAttr);
}
Below is my way to get x, y position:
$("svg").on("mousedown", function (event) {
mouseisdown = true;
var offset = $("#center").offset(); //#center is the canvas
var x = event.pageX - offset.left;
var y = event.pageY - offset.top;
current_tool("mousedown", x, y, null);
});
Above is part of my code, my problem is this function works well on the first move, but when I tried to move it second time, the element object will suddenly back to its origin location before this first move, How to fix that problem?
Any help is appreciated.
if (action == "mousedown") {
startx = x - test.transform.animVal["0"].matrix.e; //change
starty = y - test.transform.animVal["0"].matrix.f; // change2
}
if (action == "mousemove") {
if (!mouseisdown) {
return;
} else {
if (test.transform.animVal["0"]) {
console.log(test.transform.animVal["0"].matrix);
var transformX = x - startx;
var transformY = y - starty;
var transformAttr = 'translate(' + transformX + ',' + transformY + ')';
test.setAttribute('transform', transformAttr);
}
}
}
if (action == "mouseup") {
var transformX = x - startx;
var transformY = y - starty;
// console.log(x, y, startx, starty);
var transformAttr = 'translate(' + transformX + ',' + transformY + ')';
test.setAttribute('transform', transformAttr);
}
Problem solved by myself, the reason of the problem is that, when I tries the second move, the start position did not take the first transform into consider, so after the change the code in "mousedown" handler, the problem settled.
I'm making a draggable elements using interactjs.io
I need implement exactly the same behaviour that jQuery UI snap. You can see an example here in the official documentation:
The behaviour is: snaps to all other draggable elements
In interactjs.io, in the documentation, you have "Snapping" (link documentation), but I don't find the way of coding it.
I have created a fiddle here: Fiddle Link
This is my JS Code:
interact('.draggable')
.draggable({
onmove: dragMoveListener,
snap: {},
});
function dragMoveListener (event) {
var target = event.target,
// keep the dragged position in the data-x/data-y attributes
x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
// translate the element
target.style.webkitTransform =
target.style.transform =
'translate(' + x + 'px, ' + y + 'px)';
// update the position attributes
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
I need modify the snap section code, to make the draggable items snapping with others.
snap: {}
Thanks!!
The following code can give you some ideas to get the result that you want. It works with draggable elements of different sizes. Function targets are used to set the target points and lines.
You can test it in this jsfiddle.
var AXIS_RANGE = 12;
var CORNER_RANGE = 14;
var CORNER_EXCLUDE_AXIS = 8;
var AXIS_EXTRA_RANGE = -6;
var myItems = [];
var currentElement = null;
var offX1, offY1, offX2, offY2;
function getPosition(element) {
return {
x: parseFloat(element.getAttribute('data-x')) || 0,
y: parseFloat(element.getAttribute('data-y')) || 0
};
}
function isBetween(value, min, length) {
return min - AXIS_EXTRA_RANGE < value && value < (min + length) + AXIS_EXTRA_RANGE;
}
function getDistance(value1, value2) {
return Math.abs(value1 - value2);
}
function getSnapCoords(element, axis) {
var result = {
isOK: false
};
if (currentElement && currentElement !== element) {
var pos = getPosition(element);
var cur = getPosition(currentElement);
var distX1a = getDistance(pos.x, cur.x);
var distX1b = getDistance(pos.x, cur.x + currentElement.offsetWidth);
var distX2a = getDistance(pos.x + element.offsetWidth, cur.x);
var distX2b = getDistance(pos.x + element.offsetWidth, cur.x + currentElement.offsetWidth);
var distY1a = getDistance(pos.y, cur.y);
var distY1b = getDistance(pos.y, cur.y + currentElement.offsetHeight);
var distY2a = getDistance(pos.y + element.offsetHeight, cur.y);
var distY2b = getDistance(pos.y + element.offsetHeight, cur.y + currentElement.offsetHeight);
var distXa = Math.min(distX1a, distX2a);
var distXb = Math.min(distX1b, distX2b);
var distYa = Math.min(distY1a, distY2a);
var distYb = Math.min(distY1b, distY2b);
if (distXa < distXb) {
result.offX = offX1;
} else {
result.offX = offX2
}
if (distYa < distYb) {
result.offY = offY1;
} else {
result.offY = offY2
}
var distX1 = Math.min(distX1a, distX1b);
var distX2 = Math.min(distX2a, distX2b);
var distY1 = Math.min(distY1a, distY1b);
var distY2 = Math.min(distY2a, distY2b);
var distX = Math.min(distX1, distX2);
var distY = Math.min(distY1, distY2);
var dist = Math.max(distX, distY);
var acceptAxis = dist > CORNER_EXCLUDE_AXIS;
result.x = distX1 < distX2 ? pos.x : pos.x + element.offsetWidth;
result.y = distY1 < distY2 ? pos.y : pos.y + element.offsetHeight;
var inRangeX1 = isBetween(pos.x, cur.x, currentElement.offsetWidth);
var inRangeX2 = isBetween(cur.x, pos.x, element.offsetWidth);
var inRangeY1 = isBetween(pos.y, cur.y, currentElement.offsetHeight);
var inRangeY2 = isBetween(cur.y, pos.y, element.offsetHeight);
switch (axis) {
case "x":
result.isOK = acceptAxis && (inRangeY1 || inRangeY2);
break;
case "y":
result.isOK = acceptAxis && (inRangeX1 || inRangeX2);
break;
default:
result.isOK = true;
break;
}
}
return result;
}
$('.draggable').each(function() {
var pos = getPosition(this);
this.style.transform = 'translate(' + pos.x + 'px, ' + pos.y + 'px)';
myItems.push(getPosition(this));
});
interact('.draggable').draggable({
onstart: function(event) {
currentElement = event.target;
var pos = getPosition(currentElement);
offX1 = event.clientX - pos.x;
offY1 = event.clientY - pos.y;
offX2 = event.clientX - currentElement.offsetWidth - pos.x;
offY2 = event.clientY - currentElement.offsetHeight - pos.y;
},
onmove: dragMoveListener,
snap: {
targets:
(function() {
var snapPoints = [];
$('.draggable').each(function() {
(function(element) {
// Slide along the X axis
snapPoints.push(
function(x, y) {
var data = getSnapCoords(element, "x");
if (data.isOK) {
return {
x: data.x + data.offX,
range: AXIS_RANGE
};
}
});
// Slide along the Y axis
snapPoints.push(
function(x, y) {
var data = getSnapCoords(element, "y");
if (data.isOK) {
return {
y: data.y + data.offY,
range: AXIS_RANGE
};
}
});
// Snap to corner
snapPoints.push(
function(x, y) {
var data = getSnapCoords(element);
if (data.isOK) {
return {
x: data.x + data.offX,
y: data.y + data.offY,
range: CORNER_RANGE
};
}
});
})(this);
});
return snapPoints;
})()
},
onend: function(event) {
$('.draggable').each(function() {
currentElement = null;
myItems.push(getPosition(this));
});
}
});
function dragMoveListener(event) {
var target = event.target;
var oldPos = getPosition(target);
var x = oldPos.x + event.dx;
var y = oldPos.y + event.dy;
// keep the dragged position in the data-x/data-y attributes
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
// translate the element
target.style.webkitTransform =
target.style.transform =
'translate(' + x + 'px, ' + y + 'px)';
$('#position').text('x: ' + x + ' - y: ' + y);
var result = $.grep(myItems, function(e) {
if (e.x == parseInt(target.getAttribute('data-x')) || e.y == parseInt(target.getAttribute('data-y')))
return 1;
});
if (result.length >= 1)
target.style.backgroundColor = '#CCC';
else
target.style.backgroundColor = '#FFF';
}
I made a JSFiddle without using interact.js. I only used jQuery. I did not use interactjs.io as you implied that you only prefer it but don't require it.
The code works with elements of different sizes.
jQuery.fn.reverse = [].reverse;
/* Handle add button clicks*/
$(".add-draggable").click(function() {
var newDraggable = jQuery("<div class='draggable'></div>");
newDraggable.css({
position: 'absolute',
left: 150,
top: 150
})
newDraggable.attr({
'data-x': 150,
'data-y': 150
}).addClass("large");
jQuery(".draggable-wapper").append(newDraggable)
});
// initiate blocks
// This is done in revers as when the element is absolutly positioned .poisition() will retrun differnt values for the next element (mostly x will be 0 for all elements)
$(".draggable").reverse().each(function(i, e) {
_this = jQuery(this);
position = _this.position();
_this.css({
position: 'absolute',
left: position.left,
top: position.top
}).attr("data-y", position.top).attr("data-x", position.left);
});
// Set some variabkles
// Used to differentiate clicks on elements from dragging
var isDragging = false;
// Store coordiators of all elements
var coord;
// The moving element
var element = null;
// The offset to which the moving element snaps to the target element
// in percentage
var snappingYOffset = 20;
var snappingXOffset = 20;
$(".draggable-wapper").on("mousedown", ".draggable", function() {
_this = element = jQuery(this);
coord = [];
isDragging = true;
// Update coord
jQuery(".draggable").each(function(i, e) {
if (i == element.index())
return true;
ele = jQuery(e);
var position = ele.position();
var elementData = getData(ele);
coord[i] = {
leftX: position.left,
rightX: position.left + ele.outerWidth(),
topY: position.top,
bottomY: position.top + ele.outerHeight()
}
jQuery.extend(coord[i], elementData);
});
_this.removeData("last-position");
});
jQuery(document).on("mousemove", function(e) {
if (!isDragging)
return;
var lastPosition = _this.data("last-position");
element.addClass("moving");
if (typeof lastPosition != 'undefined') {
// get difference to detemine new position
var xDelta = e.clientX - lastPosition.x;
var yDelta = e.clientY - lastPosition.y;
var elementX = parseInt(element.attr("data-x"));
var elementY = parseInt(element.attr("data-y"));
element.attr({
"data-x": elementX + xDelta,
"data-y": elementY + yDelta
}).css({
"left": elementX + xDelta,
"top": elementY + yDelta
});
// find which element is closer to moving elements and within offset limits
filterArray(coord, _this);
}
// Save values for next itertation
var position = {
x: e.clientX,
y: e.clientY
};
element.data("last-position", position);
})
.on("mouseup", function() {
if (isDragging) {
isDragging = false;
element.removeClass("moving");
}
});
function filterArray(array, element) {
// Set coord for moving element
// x1: left, x2: right, y1: top, y2: bottom
var elementX1 = parseInt(element.attr("data-x"));
var elementX2 = elementX1 + element.outerWidth();
var elementY1 = parseInt(element.attr("data-y"));
var elementY2 = elementY1 + element.outerHeight();
// Show value inside element
element.html('y:' + elementY1 + '<br> x: ' + elementX1);
var result = {};
// Loop through other elements and match the closeset
array.forEach(function(value, index, originalArray) {
// Get coordinators of each element
// x1: left, x2: right, y1: top, y2: bottom
var x1 = value['leftX'];
var x2 = value['rightX'];
var y1 = value['topY'];
var y2 = value['bottomY'];
// Get which element is bigger; the moving or the target element
var biggerDim = bigger(element, {
height: value['height'],
width: value['width']
});
// Show values inside element
jQuery(".draggable").eq(index).html('y:' + y1 + '<br> x: ' + x1);
// Get offset for partiuclar element
var xOffset = value['xOffset'];
var yOffset = value['yOffset'];
// yRange checks if moving element is moving within the Y range of target element
// This requried to snap if true
var yRange = (biggerDim.height == 'moving') ? y1 >= (elementY1 - yOffset) && y2 <= (elementY2 + yOffset) : elementY1 > (y1 - yOffset) && elementY2 < (y2 + yOffset);
// xRange checks if moving element is moving within the X range of target element
// This requried to snap if true
var xRange = (biggerDim.width == 'moving') ? x1 > (elementX1 - xOffset) && x2 < (elementX2 + xOffset) : elementX1 > (x1 - xOffset) && elementX2 < (x2 + xOffset);
// Is source element (moving) within the Y range
if (yRange) {
// Is source element within right range of target
if (elementX1 >= (x2 - xOffset) && elementX1 <= (x2 + xOffset)) {
// Left side of the moving element
element.css({
"left": x2
});
// Is source element within left range of target
} else if (elementX2 >= (x1 - xOffset) && elementX2 <= (x1 + xOffset)) {
// right side of the moving element
element.css({
"left": x1 - element.outerWidth()
});
}
}
// Is source element (moving) within the X range of target
if (xRange) {
if (elementY1 >= (y2 - yOffset) && elementY1 <= (y2 + yOffset)) {
// Top side of the moving element
element.css({
"top": y2
});
} else if (elementY2 >= (y1 - yOffset) && elementY2 <= (y1 + yOffset)) {
// bottom side of the moving element
element.css({
"top": y1 - element.outerHeight()
});
}
}
});
}
/* Find which element is bigger */
function bigger(moving, target) {
var width1 = moving.outerWidth();
var height1 = moving.outerHeight();
var width2 = target.width;
var height2 = target.height;
var result = {
width: 'target',
height: 'target'
};
if (width1 > width2)
result.width = 'moving';
if (height1 > height2)
result.height = 'moving';
return result;
}
/* Get data releted to a certain element */
function getData(ele) {
var height = ele.outerHeight();
var width = ele.outerWidth();
var xOffset = (width * snappingXOffset) / 100;
var yOffset = (height * snappingYOffset) / 100;
return {
height: height,
width: width,
xOffset: xOffset,
yOffset: yOffset
}
}
.draggable {
background-color: green;
border: 1px solid white;
box-sizing: border-box;
cursor: move;
float: left;
padding: 5px;
position: relative;
color: white;
font-family: "calibri", -webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Chrome/Safari/Opera */
-khtml-user-select: none;
/* Konqueror */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
/* Non-prefixed version, currently
not supported by any browser */
}
.draggable.large {
height: 300px;
width: 100px;
font-size: 16px;
}
.draggable.small {
height: 50px;
width: 50px;
font-size: 12px;
}
.draggable.medium {
height: 100px;
width: 80px;
font-size: 12px;
}
.draggable-wapper {
float: left;
position: relative;
}
.moving {
z-index: 2;
background-color: purple;
}
.add-draggable {
background-color: green;
border: 1px solid #0a5e1d;
border-radius: 5px;
color: white;
cursor: pointer;
font-size: 19px;
padding: 10px 20px;
position: absolute;
right: 15px;
top: 15px;
transition: all 0.5s ease 0s;
font-family: "calibri",
}
.add-draggable:hover {
opacity: 0.9;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='draggable-wapper'>
<div class='draggable small'></div>
<div class='draggable large'></div>
<div class='draggable large'></div>
<div class='draggable large'></div>
<div class='draggable small'></div>
<div class='draggable medium'></div>
<div class='draggable medium'></div>
</div>
<div class='add-draggable'>
Add
</div>
I'm trying to make it so that a bunch of elements will move when the user holds down their mouse button and then drags the mouse. I'm trying to do this by listening to the mousedown and mousemove events and comparing their locations. The difference in the locations of is used to calculate how far to move all of the elements. The goal is that it will feel like all of the elements have been dragged. For some reason though, my approach results in the elements flying all over the place.
Here's an example of what I'm trying to do:
window.onload = function(){
var mouse = {isDown : false, lastX : null, lastY : null };
document.body.addEventListener("click", function(event){
var el = document.createElement("div");
el.className = "box";
el.style.left = event.clientX + "px";
el.style.top = event.clientY + "px";
el.style.backgroundColor = "BLACK";
el.style.width = "16px";
el.style.height = "16px";
el.style.position = "absolute";
document.body.appendChild(el);
});
document.body.addEventListener("mousedown", function(event){
mouse.isDown = true;
mouse.lastX = event.clientX;
mouse.lastY = event.clientY;
});
document.body.addEventListener("mouseup", function(){
mouse.isDown = false;
mouse.lastX = null;
mouse.lastY = null;
});
var removePx = function(string){return string.substring(0, string.length -2);}
document.body.addEventListener("mousemove", function(event){
console.log("move (" + event.clientX + "," + event.clientY + ")");
if(mouse.isDown){
var deltaX = event.clientX - mouse.lastX;
var deltaY = event.clientY - mouse.lastY;
console.log("DeltaX " + deltaX);
console.log("DeltaY " + deltaY);
var boxes = document.getElementsByClassName("box");
for(var i = 0; i<boxes.length; i++){
var box = boxes[i];
box.style.left = removePx(box.style.left) + deltaX + "px";
box.style.top = removePx(box.style.top) + deltaY + "px";
}
mouse.lastX = event.clientX;
mouse.lastY = event.clientY;
}
});
}
What's going wrong here? Also, is there a better way to accomplish this?
Regarding your code, i'd say that transform is preferrable regarding positioning for this kind of thing (instead of left/top) - see the discussion http://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/.
Anyway, i didn't take a closer look at your code, but instead, tried a solution myself to what i understood of it. I just used a simple pattern regarding drag and drop which is:
onmousedown - setState to moving, store the clicked point, get the current matrix of the transformation applied
onmousemove - calculate dx, dy, accumulate this differences in the matrix, update current x and y in the state
onmouseup - just remove the moving state
Here's a snippet:
var main = document.querySelector('main');
var movable = [].slice.call(document.querySelectorAll('.movable'));
var _state = {
x: 0,
y: 0,
matrix: [1, 0, 0, 1, 0, 0],
moving: false
};
movable.forEach(function (elem) {
elem.style.transform = 'matrix(' + _state.matrix.join(',') + ')'
});
function onMouseDown (e) {
_state.moving = true;
_state.x = e.clientX;
_state.y = e.clientY;
_state.matrix = matrixToArray(e.target.style.transform);
}
function matrixToArray (matrix) {
return matrix
.match(/matrix\((.*)\)/)[1]
.split(' ')
.join('')
.split(',')
.map(function (a) {
return +a;
});
}
function onMouseMove (e) {
if (!_state.moving)
return;
var dx = e.clientX - _state.x;
var dy = e.clientY - _state.y;
_state.matrix[4] += dx;
_state.matrix[5] += dy;
var transf = 'matrix(' + _state.matrix.join(',') + ')'
movable.forEach(function (elem) {
elem.style.transform = transf;
});
_state.x = e.clientX;
_state.y = e.clientY;
}
function onMouseUp (e) {
_state.moving = false;
_state.x = e.clientX;
_state.y = e.clientY;
}
main.addEventListener('mousemove', onMouseMove);
main.addEventListener('mouseup', onMouseUp);
movable.forEach(function (elem) {
elem.addEventListener('mousedown', onMouseDown);
});
html,
body,
main {
width: 100%;
height: 100%;
}
div {
width: 50px;
height: 50px;
position: absolute;
}
#d1 { background: black; left: 10px; top: 10px;}
#d2 { background: brown; left: 70px; top: 10px;}
#d3 { background: green; left: 10px; top: 70px ;}
#d4 { background: purple; left: 70px; top: 70px ;}
<main>
<div id="d1" class="movable"></div>
<div id="d2" class="movable"></div>
<div id="d3" class="not-movable"></div>
<div id="d4" class="movable"></div>
</main>
ps.: this is not all that much efficient. One can surely iterate on this and create a better version of it!
I want a pinch to Zoom function for an image. I want it to zoom in the area where the fingers are.
My index is only
<div id="wrapper" style="-webkit-transform: translate3d(0,0,0);overflow: hidden;">
</div>
And my script for zooming and scrolling is similar with this example. I have made a few changes to fit with my project
My problem is in
case 'transform':
rotation = last_rotation + ev.gesture.rotation;
scale = Math.max(1, Math.min(last_scale * ev.gesture.scale, 10));
break;
How can I change it so that it doesn't zoom into the center of the picture but at place where the first finger have touch the display?
Sorry for my bad english :)
This is an example with hammer.js and tap. As you tap it will zoom in at the point where you tapped. The event data is common for all gestures so switching from tap to pinch should work. It is a good example to work on. You may need to increase the scale step as you pinch. It has been tested on chrome(v30) and firefox (v24).
It is based on the solution mentioned at the thread,
Zoom in on a point (using scale and translate)
as you will see an alternative could also be to use canvas.
HTML
<div style="-webkit-transform: translate3d(0,0,0);overflow: hidden;" class="zoomable">
<img src="http://i.telegraph.co.uk/multimedia/archive/01842/landscape-rainbow_1842437i.jpg" />
</div>
JS
(function ($) {
$(document).ready(function () {
var scale = 1; // scale of the image
var xLast = 0; // last x location on the screen
var yLast = 0; // last y location on the screen
var xImage = 0; // last x location on the image
var yImage = 0; // last y location on the image
Hammer($('.zoomable img').get(0)).on("tap", function (event) {
var posX = event.gesture.center.pageX;
var posY = event.gesture.center.pageY;
// find current location on screen
var xScreen = posX; //- $(this).offset().left;
var yScreen = posY; //- $(this).offset().top;
// find current location on the image at the current scale
xImage = xImage + ((xScreen - xLast) / scale);
yImage = yImage + ((yScreen - yLast) / scale);
scale++;
// determine the location on the screen at the new scale
var xNew = (xScreen - xImage) / scale;
var yNew = (yScreen - yImage) / scale;
// save the current screen location
xLast = xScreen;
yLast = yScreen;
// redraw
$(this).css('-webkit-transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')')
.css('-webkit-transform-origin', xImage + 'px ' + yImage + 'px').css('-moz-transform', 'scale(' + scale + ') translate(' + xNew + 'px, ' + yNew + 'px)').css('-moz-transform-origin', xImage + 'px ' + yImage + 'px')
.css('-o-transform', 'scale(' + scale + ') translate(' + xNew + 'px, ' + yNew + 'px)').css('-o-transform-origin', xImage + 'px ' + yImage + 'px').css('transform', 'scale(' + scale + ') translate(' + xNew + 'px, ' + yNew + 'px)');
});
});
})(jQuery);
http://jsfiddle.net/SySZL/
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>