I'm trying to implement a zoom function using the mouse wheel in JointJS. The intent is to use the paper.scale() function and use the mouse coordinates for the ox & oy options. However, when I move the mouse it gets a jittery effect in the translation.
There are several zoom implementations available with a quick google search, but they all seem to suffer from the same issue.
Here is my code based on my best iterpretation of the JointJS documentation. I'm assuming the x & y are already translated to paperspace.
paper.on('blank:mousewheel', function(evt, x, y, delta) {
var normalizedDelta = Math.max(-1, Math.min(1, (delta))) / 50;
var newScale = paper.scale().sx + normalizedDelta; // the current paper scale changed by delta
if (newScale > 0.4 && newScale < 2) {
paper.translate(0, 0); // setOrigin is deprecated, replaced by translate
paper.scale(newScale, newScale, x, y);
}
})
Here is some zoom code I found by googling. It has the same effect. I've messed around with using offsetX/offsetY, local coordinates, & paper coordinates, all without luck.
paper.$el.on('mousewheel DOMMouseScroll', onMouseWheel);
function onMouseWheel(e) {
e.preventDefault();
e = e.originalEvent;
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))) / 50;
var offsetX = (e.offsetX || e.clientX - $(this).offset().left); // offsetX is not defined in FF
var offsetY = (e.offsetY || e.clientY - $(this).offset().top); // offsetY is not defined in FF
var localPoint = offsetToLocalPoint(offsetX, offsetY);
var newScale = V(paper.viewport).scale().sx + delta; // the current paper scale changed by delta
if (newScale > 0.4 && newScale < 2) {
paper.translate(0, 0); // setOrigin is deprecated, replaced by translate
paper.scale(newScale, newScale, localPoint.x, localPoint.y); //p.x, p.y);
}
}
function offsetToLocalPoint(x, y) {
var svgPoint = paper.svg.createSVGPoint();
svgPoint.x = x;
svgPoint.y = y;
// Transform point into the viewport coordinate system.
var pointTransformed = svgPoint.matrixTransform(paper.viewport.getCTM().inverse());
return pointTransformed;
}
I'm expecting this to zoom in on whatever point the mouse is located. The scaling works quite nicely when the ox & oy are set to zero. When I attempt to use the mouse coordinates for the ox & oy options, it appears to work. However, when I move the mouse around it gets a jittery translation effect. It seems like the ox & oy coordinates are delayed by one event.
Here is my attempt JSFiddle.
Here is the attempt I found via google JSFiddle
I finally did it:
paper.on("blank:mousewheel", function(evt, x, y, delta) {
evt.preventDefault();
const oldscale = paper.scale().sx;
const newscale = oldscale + 0.2 * delta * oldscale
if (newscale>0.2 && newscale<5) {
paper.scale(newscale, newscale, 0, 0);
paper.translate(-x*newscale+evt.offsetX,-y*newscale+evt.offsetY);
}
});
https://jsfiddle.net/nj5cqusg/1/
Related
I would like to achieve this effect outlined here: http://www.html5canvastutorials.com/labs/html5-canvas-multi-touch-scale-stage-with-kineticjs/
But on a Layer in KineticJS
I have already got it working to a degree (using basically the same code as in the link), but it seems the Layer scales around the 0,0 origin point and I cannot see any doco about changing the transform origin?
How can I effectively pinch and zoom a Layer such that it scales around it center point?
I made a plugin for this kind of behavior: https://github.com/eduplus/pinchlayer
It is probably a little outdated now with the recent changes, but the logic in layerTouchMove function is most likely still sound. Here:
var touch1 = event.touches[0];
var touch2 = event.touches[1];
if (touch1 && touch2) {
self.setDraggable(false);
if (self.trans != undefined) { self.trans.stop(); }
if (self.startDistance === undefined) {
self.startDistance = self.getDistance(touch1, touch2);
self.touchPosition.x = (touch1.clientX + touch2.clientX) / 2;
self.touchPosition.y = (touch1.clientY + touch2.clientY) / 2;
self.layerPosition.x = (Math.abs(self.getX()) + self.touchPosition.x) / self.startScale;
self.layerPosition.y = (Math.abs(self.getY()) + self.touchPosition.y) / self.startScale;
}
else {
var dist = self.getDistance(touch1, touch2);
var scale = (dist / self.startDistance) * self.startScale;
if (scale < self.minScale) { scale = self.minScale; }
if (scale > self.maxScale) { scale = self.maxScale; }
self.setScale(scale, scale);
var x = (self.layerPosition.x * scale) - self.touchPosition.x;
var y = (self.layerPosition.y * scale) - self.touchPosition.y;
var pos = self.checkBounds({ x: -x, y: -y });
self.setPosition(pos.x, pos.y);
self.draw();
}
Basically it will record the starting point and the distance (how long the pinch is) and then scale and set the layer position accordingly. Hope this helps.
I cannot make this ray to follow my mouse cursor.
It's always inverse on X axis.
I think that the problem is with my atan function, but since my trigonometry is fairly low, I have no idea how to fix this one.
This is how I calculate radians:
var dx = target.x - center.x;
var dy = target.y - center.y;
var rad = Math.atan2(dx, dy);
var dx = center.x - target.x;
Fiddle
The fiddle above doesn't work in Firefox as FF doesn't natively support event.offsetX/Y for mouse events, I assume your normalization code was cut off when making the minimalistic example but I'll leave my normalization code here for future reference:
function normOffset(event) {
if (typeof event.offsetX === 'undefined' || typeof event.offsetY === 'undefined') {
var targetOffset = $(event.target).offset();
event.offsetX = event.pageX - targetOffset.left;
event.offsetY = event.pageY - targetOffset.top;
}
}
Then just pass the jQuery event object to it.
Fiddle
I'm using the following functions to track mouse movement and rotate an object:
function getAngle(dx, dy) {
var angle
if (dx != 0) {
var radians = Math.atan(dy / dx) + (dx < 0 ? Math.PI : 0)
angle = radiansToDegrees(radians);
if (angle < 0) angle += 360;
} else {
angle = dy > 0 ? 90 : 270;
}
return angle;
}
function getAngleBetweenPoints(p1, p2) {
var dx = p1.x - p2.x
var dy = p1.y - p2.y
return getAngle(dx, dy)
}
$(document).mousemove(function (e) {
if (selectionBounds) {
var midpoint = new pe.Classes.Point(selectionBounds.Left + (selectionBounds.Width / 2), selectionBounds.Top + (selectionBounds.Height / 2));
var mousepoint = new pe.Classes.Point(e.pageX, e.pageY);
var angle = getAngleBetweenPoints(midpoint, mousepoint);
if (lastAngle) {
var diff = angle - lastAngle;
rotate(degreesToRadians(diff));
}
lastAngle = angle;
}
});
This works well, as long as I move the mouse slowly, and as long as the mouse doesn't get too close to the origin (midpoint). Moving too quickly causes additional spin rotations, and coming close to the origin causes unexpected changes of direction.
How can I fix this code? I really just need to know which direction the mouse is moving in (clockwise or anti-clockwise), as I can get an idea of the speed just from the change in mousepoint and then update the rotation based on that.
There are literally dozens of SO threads on topics related to this (How to get the direction (angle) of rectangle after rotating it from a pivot point, How to get cardinal mouse direction from mouse coordinates, Moving a rotated element in the direction of the rotation in JavaScript) - but I haven't been able to find anything that can answer this question, except one comment referring to this requiring the cross product, which I didn't fully understand.
http://jsfiddle.net/wRexz/3/ (click and drag to rotate the rectangle)
var angle = 0, sp = startpoint, mp = midpoint;
var p = {x:e.offsetX, y:e.offsetY};
var sAngle = Math.atan2((sp.y-mp.y),(sp.x - mp.x));
var pAngle = Math.atan2((p.y-mp.y),(p.x - mp.x));
angle = (pAngle - sAngle) * 180/Math.PI;
$("#display").text(angle);
$('#rotateme').css({ rotate: '+=' + angle });
startpoint = {x:p.x, y:p.y};
The concept here is basic trig. You find the angle from 0 of the "start point" and do the same for the "end point" or "current point". Subtract the first from the second, and that is your "delta angle".
You will still get erratic behavior around the midpoint, due to the nature of how rapidly the angles can change. One solution to this is stopping rotation when within a certain distance of the midpoint.
I'm programming a HTML5 < canvas > project that involves zooming in and out of images using the scroll wheel.
I want to zoom towards the cursor like google maps does but I'm completely lost on how to calculate the movements.
What I have: image x and y (top-left corner); image width and height; cursor x and y relative to the center of the canvas.
In short, you want to translate() the canvas context by your offset, scale() it to zoom in or out, and then translate() back by the opposite of the mouse offset. Note that you need to transform the cursor position from screen space into the transformed canvas context.
ctx.translate(pt.x,pt.y);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);
Demo: http://phrogz.net/tmp/canvas_zoom_to_cursor.html
I've put up a full working example on my website for you to examine, supporting dragging, click to zoom in, shift-click to out, or scroll wheel up/down.
The only (current) issue is that Safari zooms too fast compared to Chrome or Firefox.
I hope, these JS libraries will help you:
(HTML5, JS)
Loupe
http://www.netzgesta.de/loupe/
CanvasZoom
https://github.com/akademy/CanvasZoom
Scroller
https://github.com/zynga/scroller
As for me, I'm using loupe. It's awesome!
For you the best case - scroller.
I recently needed to archive same results as Phrogz had already done but instead of using context.scale(), I calculated each object size based on ratio.
This is what I came up with. Logic behind it is very simple. Before scaling, I calculate point distance from edge in percentages and later adjust viewport to correct place.
It took me quite a while to come up with it, hope it saves someones time.
$(function () {
var canvas = $('canvas.main').get(0)
var canvasContext = canvas.getContext('2d')
var ratio = 1
var vpx = 0
var vpy = 0
var vpw = window.innerWidth
var vph = window.innerHeight
var orig_width = 4000
var orig_height = 4000
var width = 4000
var height = 4000
$(window).on('resize', function () {
$(canvas).prop({
width: window.innerWidth,
height: window.innerHeight,
})
}).trigger('resize')
$(canvas).on('wheel', function (ev) {
ev.preventDefault() // for stackoverflow
var step
if (ev.originalEvent.wheelDelta) {
step = (ev.originalEvent.wheelDelta > 0) ? 0.05 : -0.05
}
if (ev.originalEvent.deltaY) {
step = (ev.originalEvent.deltaY > 0) ? 0.05 : -0.05
}
if (!step) return false // yea..
var new_ratio = ratio + step
var min_ratio = Math.max(vpw / orig_width, vph / orig_height)
var max_ratio = 3.0
if (new_ratio < min_ratio) {
new_ratio = min_ratio
}
if (new_ratio > max_ratio) {
new_ratio = max_ratio
}
// zoom center point
var targetX = ev.originalEvent.clientX || (vpw / 2)
var targetY = ev.originalEvent.clientY || (vph / 2)
// percentages from side
var pX = ((vpx * -1) + targetX) * 100 / width
var pY = ((vpy * -1) + targetY) * 100 / height
// update ratio and dimentsions
ratio = new_ratio
width = orig_width * new_ratio
height = orig_height * new_ratio
// translate view back to center point
var x = ((width * pX / 100) - targetX)
var y = ((height * pY / 100) - targetY)
// don't let viewport go over edges
if (x < 0) {
x = 0
}
if (x + vpw > width) {
x = width - vpw
}
if (y < 0) {
y = 0
}
if (y + vph > height) {
y = height - vph
}
vpx = x * -1
vpy = y * -1
})
var is_down, is_drag, last_drag
$(canvas).on('mousedown', function (ev) {
is_down = true
is_drag = false
last_drag = { x: ev.clientX, y: ev.clientY }
})
$(canvas).on('mousemove', function (ev) {
is_drag = true
if (is_down) {
var x = vpx - (last_drag.x - ev.clientX)
var y = vpy - (last_drag.y - ev.clientY)
if (x <= 0 && vpw < x + width) {
vpx = x
}
if (y <= 0 && vph < y + height) {
vpy = y
}
last_drag = { x: ev.clientX, y: ev.clientY }
}
})
$(canvas).on('mouseup', function (ev) {
is_down = false
last_drag = null
var was_click = !is_drag
is_drag = false
if (was_click) {
}
})
$(canvas).css({ position: 'absolute', top: 0, left: 0 }).appendTo(document.body)
function animate () {
window.requestAnimationFrame(animate)
canvasContext.clearRect(0, 0, canvas.width, canvas.height)
canvasContext.lineWidth = 1
canvasContext.strokeStyle = '#ccc'
var step = 100 * ratio
for (var x = vpx; x < width + vpx; x += step) {
canvasContext.beginPath()
canvasContext.moveTo(x, vpy)
canvasContext.lineTo(x, vpy + height)
canvasContext.stroke()
}
for (var y = vpy; y < height + vpy; y += step) {
canvasContext.beginPath()
canvasContext.moveTo(vpx, y)
canvasContext.lineTo(vpx + width, y)
canvasContext.stroke()
}
canvasContext.strokeRect(vpx, vpy, width, height)
canvasContext.beginPath()
canvasContext.moveTo(vpx, vpy)
canvasContext.lineTo(vpx + width, vpy + height)
canvasContext.stroke()
canvasContext.beginPath()
canvasContext.moveTo(vpx + width, vpy)
canvasContext.lineTo(vpx, vpy + height)
canvasContext.stroke()
canvasContext.restore()
}
animate()
})
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
<canvas class="main"></canvas>
</body>
</html>
I took #Phrogz's answer as a basis and made a small library that enables canvas with dragging, zooming and rotating.
Here is the example.
var canvas = document.getElementById('canvas')
//assuming that #param draw is a function where you do your main drawing.
var control = new CanvasManipulation(canvas, draw)
control.init()
control.layout()
//now you can drag, zoom and rotate in canvas
You can find more detailed examples and documentation on the project's page
Faster
Using ctx.setTransform gives you more performance than multiple matrix calls ctx.translate, ctx.scale, ctx.translate.
No need for complex transformation inversions as and expensive DOM matrix calls tp converts point between zoomed and screen coordinate systems.
Flexible
Flexibility as you don't need to use ctx.save and ctx.restore if you are rendering content at using different transforms. Returning to the transform with ctx.setTransform rather than the potentially frame rate wreaking ctx.restorecall
Easy to invert the transform and get the world coordinates of a (screen) pixel position and the other way round.
Examples
Using mouse and mouse wheel to zoom in and out at mouse position
An example using this method to scale page content at a point (mouse) via CSS transform CSS Demo at bottom of answer also has a copy of the demo from the next example.
And an example of this method used to scale canvas content at a point using setTransform
How
Given a scale and pixel position you can get the new scale as follow...
const origin = {x:0, y:0}; // canvas origin
var scale = 1; // current scale
function scaleAt(x, y, scaleBy) { // at pixel coords x, y scale by scaleBy
scale *= scaleBy;
origin.x = x - (x - origin.x) * scaleBy;
origin.y = y - (y - origin.y) * scaleBy;
}
To position the canvas and draw content
ctx.setTransform(scale, 0, 0, scale, origin.x, origin.y);
ctx.drawImage(img, 0, 0);
To use if you have the mouse coordinates
const zoomBy = 1.1; // zoom in amount
scaleAt(mouse.x, mouse.y, zoomBy); // will zoom in at mouse x, y
scaleAt(mouse.x, mouse.y, 1 / zoomBy); // will zoom out by same amount at mouse x,y
To restore the default transform
ctx.setTransform(1,0,0,1,0,0);
The inversions
To get the coordinates of a point in the zoomed coordinate system and the screen position of a point in the zoomed coordinate system
Screen to world
function toWorld(x, y) { // convert to world coordinates
x = (x - origin.x) / scale;
y = (y - origin.y) / scale;
return {x, y};
}
World to screen
function toScreen(x, y) {
x = x * scale + origin.x;
y = y * scale + origin.y;
return {x, y};
}
This question already has answers here:
How do I get the coordinates of a mouse click on a canvas element? [duplicate]
(22 answers)
Closed 3 years ago.
Is there a way to get the location mouse inside a <canvas> tag? I want the location relative to to the upper right corner of the <canvas>, not the entire page.
The accepted answer will not work every time. If you don't use relative position the attributes offsetX and offsetY can be misleading.
You should use the function: canvas.getBoundingClientRect() from the canvas API.
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
canvas.addEventListener('mousemove', function(evt) {
var mousePos = getMousePos(canvas, evt);
console.log('Mouse position: ' + mousePos.x + ',' + mousePos.y);
}, false);
Easiest way is probably to add a onmousemove event listener to the canvas element, and then you can get the coordinates relative to the canvas from the event itself.
This is trivial to accomplish if you only need to support specific browsers, but there are differences between f.ex. Opera and Firefox.
Something like this should work for those two:
function mouseMove(e)
{
var mouseX, mouseY;
if(e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
else if(e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
/* do something with mouseX/mouseY */
}
Also note that you'll need CSS:
position: relative;
set to your canvas tag, in order to get the relative mouse position inside the canvas.
And the offset changes if there's a border
I'll share the most bulletproof mouse code that I have created thus far. It works on all browsers will all manner of padding, margin, border, and add-ons (like the stumbleupon top bar)
// Creates an object with x and y defined,
// set to the mouse position relative to the state's canvas
// If you wanna be super-correct this can be tricky,
// we have to worry about padding and borders
// takes an event and a reference to the canvas
function getMouse = function(e, canvas) {
var element = canvas, offsetX = 0, offsetY = 0, mx, my;
// Compute the total offset. It's possible to cache this if you want
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Add padding and border style widths to offset
// Also add the <html> offsets in case there's a position:fixed bar (like the stumbleupon bar)
// This part is not strictly necessary, it depends on your styling
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
// We return a simple javascript object with x and y defined
return {x: mx, y: my};
}
You'll notice that I use some (optional) variables that are undefined in the function. They are:
stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingLeft'], 10) || 0;
stylePaddingTop = parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingTop'], 10) || 0;
styleBorderLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)['borderLeftWidth'], 10) || 0;
styleBorderTop = parseInt(document.defaultView.getComputedStyle(canvas, null)['borderTopWidth'], 10) || 0;
// Some pages have fixed-position bars (like the stumbleupon bar) at the top or left of the page
// They will mess up mouse coordinates and this fixes that
var html = document.body.parentNode;
htmlTop = html.offsetTop;
htmlLeft = html.offsetLeft;
I'd recommend only computing those once, which is why they are not in the getMouse function.
For mouse position, I usually use jQuery since it normalizes some of the event attributes.
function getPosition(e) {
//this section is from http://www.quirksmode.org/js/events_properties.html
var targ;
if (!e)
e = window.event;
if (e.target)
targ = e.target;
else if (e.srcElement)
targ = e.srcElement;
if (targ.nodeType == 3) // defeat Safari bug
targ = targ.parentNode;
// jQuery normalizes the pageX and pageY
// pageX,Y are the mouse positions relative to the document
// offset() returns the position of the element relative to the document
var x = e.pageX - $(targ).offset().left;
var y = e.pageY - $(targ).offset().top;
return {"x": x, "y": y};
};
// now just make sure you use this with jQuery
// obviously you can use other events other than click
$(elm).click(function(event) {
// jQuery would normalize the event
position = getPosition(event);
//now you can use the x and y positions
alert("X: " + position.x + " Y: " + position.y);
});
This works for me in all the browsers.
EDIT:
I copied the code from one of my classes I was using, so the jQuery call to this.canvas was wrong. The updated function figures out which DOM element (targ) caused the event and then uses that element's offset to figure out the correct position.
GEE is an endlessly helpful library for smoothing out troubles with canvas, including mouse location.
Simple approach using mouse event and canvas properties:
JSFiddle demo of functionality http://jsfiddle.net/Dwqy7/5/
(Note: borders are not accounted for, resulting in off-by-one):
Add a mouse event to your canvas
canvas.addEventListener("mousemove", mouseMoved);
Adjust event.clientX and event.clientY based on:
canvas.offsetLeft
window.pageXOffset
window.pageYOffset
canvas.offsetTop
Thus:
canvasMouseX = event.clientX - (canvas.offsetLeft - window.pageXOffset);
canvasMouseY = event.clientY - (canvas.offsetTop - window.pageYOffset);
The original question asked for coordinates from the upper right (second function).
These functions will need to be within a scope where they can access the canvas element.
0,0 at upper left:
function mouseMoved(event){
var canvasMouseX = event.clientX - (canvas.offsetLeft - window.pageXOffset);
var canvasMouseY = event.clientY - (canvas.offsetTop - window.pageYOffset);
}
0,0 at upper right:
function mouseMoved(event){
var canvasMouseX = canvas.width - (event.clientX - canvas.offsetLeft)- window.pageXOffset;
var canvasMouseY = event.clientY - (canvas.offsetTop - window.pageYOffset);
}
I'd use jQuery.
$(document).ready(function() {
$("#canvas_id").bind( "mousedown", function(e){ canvasClick(e); } );
}
function canvasClick( e ){
var x = e.offsetX;
var y = e.offsetY;
}
This way your canvas can be anywhere on your page, relative or absolute.
Subtract the X and Y offsets of the canvas DOM element from the mouse position to get the local position inside the canvas.