Resize multiple objects with JS considering rotate - javascript

I'm working on visual editor with objects and user interactions around like move, resize, rotate, etc...
I have resize and rotate functionality in place. Now I have implemented multi-select functionality when user select multiple objects and resize objects keeping the original proportion.
That functionality works very well, however not for rotated objects. I've created a simplified codepen example. Basically the question is - how to adjust resize() function to make sure it works well for rotated objects. To reproduce an issue just click on "Rotate" and then "Increase width & height" once or multiple times.
function resize(incrementX, incrementY, offsetX, offsetY) {
...
}

I'm not sure if this is a valid solution for your problem, but you can undo the rotation before resizing, and reset the rotation afterwards. Like this.
function resize(incrementX, incrementY, offsetX, offsetY) {
var old_r = objmultiple.r
rotate(-objmultiple.r)
var ratioX = (objmultiple.w + incrementX) / objmultiple.w;
var ratioY = (objmultiple.h + incrementY) / objmultiple.h;
objmultiple.x += offsetX;
objmultiple.y += offsetY;
objmultiple.w = objmultiple.w + incrementX;
objmultiple.h = objmultiple.h + incrementY;
[obj1, obj2].forEach(function(obj) {
obj.x = (obj.x - objmultiple.x + offsetX) * ratioX + objmultiple.x;
obj.y = (obj.y - objmultiple.y + offsetY) * ratioY + objmultiple.y;
obj.w *= ratioX;
obj.h *= ratioY;
});
rotate(old_r)
}
Codepen here

Related

Slow the movement of circle collision resolution

I have written a collision detection and resolution system for the game I am currently working on. The collision resolution doesn't work quite as intended though. Occasionally I spawn one circle inside of the other, when this happens I'd like the inside circle to slowly move out of the other's diameter, but currently the code that I have working does it quite quickly and jarringly. It instantly teleports outside of the circle, rather than a slow transition.
I know that the code I am currently using tells the circle where it SHOULD be, so I just need to slowly move the circle to that position. But that is proving to be difficult.
I've tried a couple of solutions, one is included in the code below. I have also tried using linear interpolation to move the circle to its current position, to where the collision algorithm is telling it it should be. That didn't work correctly either.
//r is the radius of the circle
var dx = cell.x - cell2.x;
var dy = cell.y - cell2.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < cell.r + cells2.r && cell.r > cells2.r){
var unitX = dx/distance;
var unitY = dy/distance;
cell.x = cells2.x + (cell.r + cells2.r + 1) * unitX;
cell.y = cells2.y + (cell.r + cells2.r + 1) * unitY;
//Ive tried below, but the results were not correct.
//cell.x += (cells2.x + (cell.r + cells2.r + 1) * unitX)*0.5;
//cell.y += (cells2.y + (cell.r + cells2.r + 1) * unitY)*0.5;
}
To move cell apart from cell2, you don't need to add cells2 coordinates
cell.x += (cell.r + cells2.r + 1) * unitX;
To move slowly, make speed smaller
//calculate once:
v = 0.01 * (cell.r + cells2.r - distance);
// at every step:
cell.x += v * unitX;

Making element rotate to mouse position, after another rotation

I'm trying to make a tank-simulator of sorts. It's strictly on divs, not on canvas etc. It's already rotating and moving forward/backward how I want it to, but rotating the turret is a bit problematic. I got to the point where the turret rotates to the mouse pointer, however when the tank body rotates, the turret rotation doesn't take it into account (it rotates with the tanks body). Does anyone have any idea how to make it work so that the turret ALWAYS points to the mouse cursor?
Preferably in pure JS.
It's a bit too big to paste, so I'll just link to fiddle:
https://jsfiddle.net/9pom714a/
Sample of mousemove handler:
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX - field.offsetLeft;
mouseY = e.clientY - field.offsetTop;
turretAngle = Math.atan2(mouseX - turretBaseX, -(mouseY - turretBaseY)) * (180/Math.PI);
turretBase.style.transform = 'rotate(' + turretAngle + 'deg)';
})
I know the code is a mess, I'll refactor it after I get everything to work ;)
substract the tankAngle to the turretAngle rotation like this:
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX - field.offsetLeft;
mouseY = e.clientY - field.offsetTop;
turretAngle = Math.atan2(mouseX - turretBaseX, -(mouseY - turretBaseY)) * (180/Math.PI);
turretAngle -= tankAngle;
turretBase.style.transform = 'rotate(' + turretAngle + 'deg)';
})
Updated fiddle: https://jsfiddle.net/9pom714a/1/

Snap SVG: Dragging group doesn't update elements

JSFiddle here: JSFiddle
When dragging a group of objects, the individual objects' location attributes don't seem to be getting updated. This occurs whether I use the default drag() handler or define my own. Even the group BBox operation doesn't seem to update. Code:
var s = Snap("#svg");
var move = function (dx, dy, posx, posy) {
this.attr({
x: posx,
y: posy
});
//this.transform("t" + dx + "," + dy);
};
var block = s.rect(100, 100, 100, 100);
var circle = s.circle(100, 100, 50);
var group = s.g(block, circle);
//group.drag(move, function () {}, function () {});
group.drag();
//block.drag(move, function () {}, function () {});
//just a way to keep info coming w/o an interminable script
document.addEventListener('mousemove', function (e) {
bbox = block.getBBox();
block_x = block.attr("x");
block_y = block.attr("y");
gbbox = group.getBBox();
console.log("block is at " + block_x + "," + block_y,
" Block Bbbox is at " + bbox.x + "," + bbox.y,
" Group Bbbox is at " + gbbox.x + "," + gbbox.y);
}, false);
If I define only one object (say, a rect) and leave it out of a group, and pass my own "move" function to the call to drag, and include setting the "x" and "y" attributes explicitly, then that works. But if I include the rect in a group, then...I can't figure out how to do it, and I've tried a few ways (see the multiple commented-out lines showing things I've tried). I need to know where the rect sub-group element ends up after the drag, or at least the BBox of the whole group. Neither of these seem to be getting updated -- i.e. the console log I put in shows the same numbers forever, no matter where I move the object(s).
Can anyone help?
JSFiddle here: JSFiddle
I think this is because they are two different things, so they aren't actually interchangable.
The drag handler uses transforms. A transform doesn't affect any other attributes, its just an attribute on an element (in this case the group element).
getBBox will work in its current transform space, note this may be different to the clients (eg if the svg were zoomed in/out). So they are two slightly different methods, that do different things.
Use getBoundingClientRect if you need a bounding box relative to the client window. Use getBBox if you need a bounding box in the elements current coordinate space.
Code is using snap.svg.zpd as well, so zoom is possible. Problem is at onStopMove function. Events are fired when group is moved arround. In group is one circle(this.select('#main-inner-circle')) which does not have predefined location inside group. Im trying to get correct cx and cy of that inner circle after moving group.
self.onMove = function (dx, dy, ev, x, y) {
var clientX, clientY;
var tdx, tdy;
if ((typeof dx == 'object') && (dx.type == 'touchmove')) {
clientX = dx.changedTouches[0].clientX;
clientY = dx.changedTouches[0].clientY;
dx = clientX - this.data('ox');
dy = clientY - this.data('oy');
}
var snapInvMatrix = this.transform().diffMatrix.invert();
snapInvMatrix.e = snapInvMatrix.f = 0;
tdx = snapInvMatrix.x(dx, dy);
tdy = snapInvMatrix.y(dx, dy);
this.transform("t" + [tdx, tdy] + this.data('ot'));
}
self.onStartMove = function (x, y, ev) {
if ((typeof x == 'object') && (x.type == 'touchstart')) {
x.preventDefault();
this.data('ox', x.changedTouches[0].clientX);
this.data('oy', x.changedTouches[0].clientY);
}
this.data('ot', this.transform().local);
if (callbacks.onStartMove) {
callbacks.onStartMove();
}
}
self.onStopMove = function () {
var self = this.select('#main-inner-circle');
this.data('ot', this.transform().local);
//self.data('ot', self.transform().local);
console.log(self.getTransformedBBox());
console.log(this.getBBox());
//console.log($(self.node).offset().left - $(self.node).parent().offset().left);
var bBox = this.getBBox();
//var x = bBox.x + $(self.node).offset().left - $(self.node).parent().offset().left + self.getBBox().width / 2;
//var y = bBox.y + $(self.node).offset().top - $(self.node).parent().offset().top + self.getBBox().height / 2;
model.updateElementCoordinates(index, $(this.node).attr("rel"), { x: self.getTransformedBBox().cx, y: self.getTransformedBBox().cy });
if (callbacks.onStopMove) {
callbacks.onStopMove();
}
}
In order to post this question, I'd created the JSFiddle but left out the crucial snap.svg definitions...
<script src="http://snapsvg.io/assets/js/snap.svg-min.js"></script>
...with that, then indeed the group.getBBox() method actually works. However:
Apparently, using getBBox() is incredibly slow -- much slower than just accessing a "x" attribute of something like I was doing before grouping objects. All I know is that my code slows to a crawl if I use getBBox() (I have a lot of objects on the screen).
Further down in the same post mentioned earier ["Get coordinates of svg group on drag with snap.svg"1 recommended getBoundingClientRect(), which also works fine AND is fast enough! My new, working Fiddle showing all of these methods is here: New JSFiddle.
So, future users: use .node.getBoundingClientRect().

Canvas within a scrollable browser window (grab position)

I have an issue when drawing in a canvas within a browser window that has a vertical scrollbar.
The figures is at the correct position, and is possible to grab it around the canvas and make the connections, but this is only possible with the vertical scrollbar (of the browser window) fully up.
When the window is scrolled down, the nodes can't be dragged any more, and even the cursor doest change when its hovering the node.
I figured out that its possible to drag the node when scrolled down. Somehow, the "grabbing area" of the node doesn't change its position, as if this area had a fixed position according to the browser window.
What I'm doing wrong?
obs.: Cant post images :(, I don't have enough reputation.
Thanks in advance!
You basically need to modify that code to offset page scroll position
canvas.fromDocumentToCanvasCoordinate = $.proxy(function(x, y) {
return new draw2d.geo.Point(
(x + window.pageXOffset - this.getAbsoluteX() + this.getScrollLeft())*this.zoomFactor,
(y + window.pageYOffset - this.getAbsoluteY() + this.getScrollTop())*this.zoomFactor);
},canvas);
canvas.fromCanvasToDocumentCoordinate = $.proxy(function(x,y) {
return new draw2d.geo.Point(
((x*(1/this.zoomFactor)) + this.getAbsoluteX() - this.getScrollLeft() - window.pageXOffset),
((y*(1/this.zoomFactor)) + this.getAbsoluteY() - this.getScrollTop() - window.pageYOffset));
},canvas);
I posted the same question in the google group of Draw2d and receive the following answer from the framework developer, Andreas Herz.
"Hi
this is small design flaw in the lib.
normaly it is possible to "autodetect" the scroll position of the div/canvas. But i didn't it currently.
Solution:
EITHER: set the scroll container in the draw2d.Canvas with the method Canvas#setScrollArea(DOMNode node)
OR: you calculate by your own if the first solution didn't work
var canvas = new draw2d.Canvas("domId");
canvas.fromDocumentToCanvasCoordinate = $.proxy(function(x, y) {
return new draw2d.geo.Point(
(x - this.getAbsoluteX() + this.getScrollLeft())*this.zoomFactor,
(y - this.getAbsoluteY() + this.getScrollTop())*this.zoomFactor);
},canvas);
/**
* #method
* Transforms a canvas coordinate to document coordinate.
*
* #param {Number} x the x coordinate in the canvas
* #param {Number} y the y coordinate in the canvas
*
* #returns {draw2d.geo.Point} the coordinate in relation to the document [0,0] position
*/
canvas.fromCanvasToDocumentCoordinate = $.proxy(function(x,y) {
return new draw2d.geo.Point(
((x*(1/this.zoomFactor)) + this.getAbsoluteX() - this.getScrollLeft()),
((y*(1/this.zoomFactor)) + this.getAbsoluteY() - this.getScrollTop()));
},canvas);"
slight modification of Vinícius Oliveira's answer worked for me:
canvas = new draw2d.Canvas("canvas_id");
canvas.fromDocumentToCanvasCoordinate = $.proxy(function(x, y) {
return new draw2d.geo.Point(
(x - this.getAbsoluteX() + $(document).scrollLeft())*this.zoomFactor,
(y - this.getAbsoluteY() + $(document).scrollTop())*this.zoomFactor);
},canvas);
canvas.fromCanvasToDocumentCoordinate = $.proxy(function(x,y) {
return new draw2d.geo.Point(
((x*(1/this.zoomFactor)) + this.getAbsoluteX() - $(document).scrollLeft()),
((y*(1/this.zoomFactor)) + this.getAbsoluteY() - $(document).scrollTop()));
},canvas);
I used scrollLeft() and scrollTop() instead of getScrollLeft() and getScrollTop(). Also I put $(document) instead of "this".

Zoom image to cursor breaks when mouse is moved

This is a followup question to How to zoom to mouse pointer while using my own mousewheel smoothscroll?
I am using css transforms to zoom an image to the mouse pointer. I am also using my own smooth scroll algorithm to interpolate and provide momentum to the mousewheel.
With Bali Balo's help in my previous question I have managed to get 90% of the way there.
You can now zoom the image all the way in to the mouse pointer while still having smooth scrolling as the following JSFiddle illustrates:
http://jsfiddle.net/qGGwx/7/
However, the functionality is broken when the mouse pointer is moved.
To further clarify, If I zoom in one notch on the mousewheel the image is zoomed around the correct position. This behavior continues for every notch I zoom in on the mousewheel, completely as intended. If however, after zooming part way in, I move the mouse to a different position, the functionality breaks and I have to zoom out completely in order to change the zoom position.
The intended behavior is for any changes in mouse position during the zooming process to be correctly reflected in the zoomed image.
The two main functions that control the current behavior are as follows:
self.container.on('mousewheel', function (e, delta) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta
self.smoothWheel(delta);
return false;
});
This function collects the current position of the mouse at the current scale of the zoomed image.
It then starts my smooth scroll algorithm which results in the next function being called for every interpolation:
zoom: function (scale) {
var self = this;
self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);
var compat = ['-moz-', '-webkit-', '-o-', '-ms-', ''];
var newCss = {};
for (var i = compat.length - 1; i; i--) {
newCss[compat[i] + 'transform'] = 'scale(' + scale + ')';
newCss[compat[i] + 'transform-origin'] = self.currentLocation.x + 'px ' + self.currentLocation.y + 'px';
}
self.image.css(newCss);
self.currentscale = scale;
},
This function takes the scale amount (1-10) and applies the css transforms, repositioning the image using transform-origin.
Although this works perfectly for a stationary mouse position chosen when the image is completely zoomed out; as stated above it breaks when the mouse cursor is moved after a partial zoom.
Huge thanks in advance to anyone who can help.
Actually, not too complicated. You just need to separate the mouse location updating logic from the zoom updating logic. Check out my fiddle:
http://jsfiddle.net/qGGwx/41/
All I have done here is add a 'mousemove' listener on the container, and put the self.mouseLocation updating logic in there. Since it is no longer required, I also took out the mouseLocation updating logic from the 'mousewheel' handler. The animation code stays the same, as does the decision of when to start/stop the animation loop.
here's the code:
self.container.on('mousewheel', function (e, delta) {
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta
self.smoothWheel(delta);
return false;
});
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
});
Before you check this fiddle out; I should mention:
First of all, within your .zoom() method; you shouldn't divide by currentscale:
self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);
because; you already use that factor when calculating the mouseLocation inside the initmousewheel() method like this:
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
So instead; (in the .zoom() method), you should:
self.currentLocation.x += (self.mouseLocation.x - self.currentLocation.x);
self.currentLocation.y += (self.mouseLocation.y - self.currentLocation.y);
But (for example) a += b - a will always produce b so the code above equals to:
self.currentLocation.x = self.mouseLocation.x;
self.currentLocation.y = self.mouseLocation.y;
in short:
self.currentLocation = self.mouseLocation;
Then, it seems you don't even need self.currentLocation. (2 variables for the same value). So why not use mouseLocation variable in the line where you set the transform-origin instead and get rid of currentLocation variable?
newCss[compat[i] + 'transform-origin'] = self.mouseLocation.x + 'px ' + self.mouseLocation.y + 'px';
Secondly, you should include a mousemove event listener within the initmousewheel() method (just like other devs here suggest) but it should update the transform continuously, not just when the user wheels. Otherwise the tip of the pointer will never catch up while you're zooming out on "any" random point.
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale);
});
So; you wouldn't need to calculate this anymore within the mousewheel event handler so, your initmousewheel() method would look like this:
initmousewheel: function () {
var self = this;
self.container.on('mousewheel', function (e, delta) {
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta;
self.smoothWheel(delta);
return false;
});
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale); // <--- update transform origin dynamically
});
}
One Issue:
This solution works as expected but with a small issue. When the user moves the mouse in regular or fast speed; the mousemove event seems to miss the final position (tested in Chrome). So the zooming will be a little off the pointer location. Otherwise, when you move the mouse slowly, it gets the exact point. It should be easy to workaround this though.
Other Notes and Suggestions:
You have a duplicate property (prevscale).
I suggest you always use JSLint or JSHint (which is available on
jsFiddle too) to validate your code.
I highly suggest you to use closures (often refered to as Immediately Invoked Function Expression (IIFE)) to avoid the global scope when possible; and hide your internal/private properties and methods.
Add a mousemover method and call it in the init method:
mousemover: function() {
var self = this;
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale);
});
},
Fiddle: http://jsfiddle.net/powtac/qGGwx/34/
Zoom point is not exactly right because of scaling of an image (0.9 in ratio). In fact mouse are pointing in particular point in container but we scale image. See this fiddle http://jsfiddle.net/qGGwx/99/ I add marker with position equal to transform-origin. As you can see if image size is equal to container size there is no issue. You need this scaling? Maybe you can add second container? In fiddle I also added condition in mousemove
if(self.running && self.currentscale>1 && self.currentscale != self.lastscale) return;
That is preventing from moving image during zooming but also create an issue. You can't change zooming point if zoom is still running.
Extending #jordancpaul's answer I have added a constant mouse_coord_weight which gets multiplied to delta of the mouse coordinates. This is aimed at making the zoom transition less responsive to the change in mouse coordinates. Check it out http://jsfiddle.net/7dWrw/
I have rewritten the onmousemove event hander as:
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
console.log(offset);
var x = (e.pageX - offset.left) / self.currentscale,
y = (e.pageY - offset.top) / self.currentscale;
if(self.running) {
self.mouseLocation.x += (x - self.mouseLocation.x) * self.mouse_coord_weight;
self.mouseLocation.y += (y - self.mouseLocation.y) * self.mouse_coord_weight;
} else {
self.mouseLocation.x = x;
self.mouseLocation.y = y;
}
});

Categories