I'm trying to implement functionality to "pan" inside a canvas in HTML5 and I am unsure about the best way to go about accomplishing it.
Currently - I am trying to detect where the mouse is on the canvas, and if it is within 10% of an edge, it will move in that direction, as shown:
Current Edge Detection:
canvas.onmousemove = function(e)
{
var x = e.offsetX;
var y = e.offsetY;
var cx = canvas.width;
var cy = canvas.height;
if(x <= 0.1*cx && y <= 0.1*cy)
{
alert("Upper Left");
//Move "viewport" to up and left (if possible)
}
//Additional Checks for location
}
I know I could probably accomplish this by creating paths within the canvas and attaching events to them, but I haven't worked with them much, so I thought I would ask here. Also - if a "wrapping" pan would be possible that would be awesome (panning to the left will eventually get to the right).
Summary: I am wondering what the best route is to accomplish "panning" is within the HTML5 Canvas. This won't be using images but actual drawn objects (if that makes any difference). I'll be glad to answer any questions if I can.
Demo:
Demo
It depends on how you want panning with mouse movement to be implemented, but today it's often 'realtime' panning in that you can drag around. I tried to update your fiddle a little: http://jsfiddle.net/pimvdb/VWn6t/3/.
var isDown = false; // whether mouse is pressed
var startCoords = []; // 'grab' coordinates when pressing mouse
var last = [0, 0]; // previous coordinates of mouse release
canvas.onmousedown = function(e) {
isDown = true;
startCoords = [
e.offsetX - last[0], // set start coordinates
e.offsetY - last[1]
];
};
canvas.onmouseup = function(e) {
isDown = false;
last = [
e.offsetX - startCoords[0], // set last coordinates
e.offsetY - startCoords[1]
];
};
canvas.onmousemove = function(e)
{
if(!isDown) return; // don't pan if mouse is not pressed
var x = e.offsetX;
var y = e.offsetY;
// set the canvas' transformation matrix by setting the amount of movement:
// 1 0 dx
// 0 1 dy
// 0 0 1
ctx.setTransform(1, 0, 0, 1,
x - startCoords[0], y - startCoords[1]);
render(); // render to show changes
}
pimvdb's fiddle shows the concept nicely but doesn't actually work, at least not for me.
Here's a fixed version: http://jsfiddle.net/VWn6t/173/
The meat of it is basically the same.
var startCoords = {x: 0, y: 0};
var last = {x: 0, y: 0};
var isDown = false;
canvas.onmousemove = function (e) {
if(isDown) {
ctx.setTransform(1, 0, 0, 1,
xVal - startCoords.x,
yVal - startCoords.y);
}
};
canvas.onmousedown = function (e) {
isDown = true;
startCoords = {x: e.pageX - this.offsetLeft - last.x,
y: e.pageY - this.offsetTop - last.y};
};
canvas.onmouseup = function (e) {
isDown = false;
last = {x: e.pageX - this.offsetLeft - startCoords.x,
y: e.pageY - this.offsetTop - startCoords.y};
};
Related
What I mean is that the user presses a mouse button at point xy on an HTML canvas and while the mouse button is pressed the rectangle can be resized according to the movement of the cursor with point xy fixed. Like how highlighting works.
This is what I've got so far but it doesn't seem to be working:
canvas.addEventListener('mousedown', function(e){
var rectx = e.clientX;
var recty = e.clientY;
canvas.onmousemove = function(e){
var df = e.clientX;
var fg = e.clientY;
};
context.rect(rectx, recty, df-rectx, fg-recty);
context.stroke();
}, false);
Assuming there are no transforms (scale, translate) on your canvas context.
Basic steps for having a resizable rectangle are as follows:
Create a mousedown listener that sets a flag indicating the use is holding down the mouse button, as well as sets the "anchor," or initial coordinates.
Create a mouseup listener that unsets the flag.
Create a mousemove listener that, if the flag indicates the mouse is down, redraws the canvas with the rectangle's size changed according to mouse coordinates.
An important note is that client coordinates in the event object are relative to the page, not to your canvas element. You will frequently need to convert clientX and clientY into canvas coordinates:
var getCanvasCoords = function (clientX, clientY) {
var rect = canvas.getBoundingClientRect();
return {
x: clientX - rect.left,
y: clientY - rect.top
};
};
The first two steps look something like this:
var anchorX;
var anchorY;
var mouseDown = false;
canvas.addEventListener('mousedown', function (event) {
var coords = getCanvasCoords(event.clientX, event.clientY);
anchorX = coords.x;
anchorY = coords.y;
mouseDown = true;
});
canvas.addEventListener('mouseup', function (event) {
mouseDown = false;
});
And the mousemove handler:
canvas.addEventListener('mousemove', function (event) {
var coords = getCanvasCoords(event.clientX, event.clientY);
var width = coords.x - anchorX;
var height = coords.y - anchorY;
// clear canvas for redrawing
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillRect(anchorX, anchorY, width, height);
});
Don't Render from mouse events!
The given answer is correct but it is not the best way to do this.
The are two reasons. First the mousemove event can fire up to 600+ times a second but the display only refreshes 60 times a second. Rendering from the input event is many time just a waste of CPU time as the results will be overwritten by the next mouse event before it is ever had a chance to be displayed.
The second reason is that dedicating an event listener to a single task makes it hard to add more functionality. You end up adding more and more code to the mousemove event to handle all the types of input, most of which can be ignored because of the high update speed of the mouse.
Mouse listeners
Mouse event listeners do the minimum possible. They just record the mouse state and no more. Also all mouse events return the mouse position. You should not ignore the mouse position for events like mouse down and up
The following function creates a mouse object for a element. The mouse object has the x,y position relative to the to left of the element, and the current button states for 3 buttons it is left to right button1, button2, button3.
Also when the mouse leaves the element and then releases the mouse button the mouse for the element will not see the mouseup event and not know the mouse button is up. To prevent the mouse buttons from getting stuck you turn off the buttons when the mouse leaves the element.
The best mouse listener is to the whole page as it can track mouse events that happen even when the mouse is outside the window/tab (if the window/tab has focus), but that is a little to complex for this answer.
Function to create a mouse for an element
function createMouse(element){
var mouse = {
x : 0,
y : 0,
button1 : false,
button2 : false,
button3 : false,
over : false,
};
function mouseEvent(event){
var bounds = element.getBoundingClientRect();
mouse.x = event.pageX - bounds.left - scrollX;
mouse.y = event.pageY - bounds.top - scrollY;
if(event.type === "mousedown"){
mouse["button"+event.which] = true;
} else if(event.type === "mouseup"){
mouse["button"+event.which] = false;
} else if(event.type === "mouseover"){
mouse.over = true;
} else if(event.type === "mouseout"){
mouse.over = false;
mouse.button1 = false; // turn of buttons to prevent them locking
mouse.button2 = false;
mouse.button3 = false;
}
event.preventDefault(); // stops default mouse behaviour.
}
var events = "mousemove,mousedown,mouseup,mouseout,mouseover".split(',');
events.forEach(eventType => element.addEventListener(eventType,mouseEvent));
mouse.remove = function(){
events.forEach(eventType => element.removeEventListener(eventType, mouseEvent));
}
return mouse;
}
Using the mouse
It is now just a matter of creating a mouse for the element
var canMouse = createMouse(canvas);
And then in your main render loop do the dragging.
var drag = {
x : 0,
y : 0,
x1 : 0,
y1 : 0,
dragging : false,
top : 0,
left : 0,
width : 0,
height : 0,
}
function mainLoop(){
if(canMouse.button1){ // is button down
if(!drag.dragging){ // is dragging
drag.x = canMouse.x;
drag.y = canMouse.y;
drag.dragging = true;
}
drag.x1 = canMouse.x;
drag.y1 = canMouse.y;
drag.top = Math.min(drag.y, drag.y1);
drag.left = Math.min(drag.x, drag.x1);
drag.width = Math.abs(drag.x - drag.x1);
drag.height = Math.abs(drag.y - drag.y1);
}else{
if(drag.dragging){
drag.dragging = false;
}
}
}
Putting it all together
function createMouse(element){
var mouse = {
x : 0,
y : 0,
button1 : false,
button2 : false,
button3 : false,
over : false,
};
function mouseEvent(event){
var bounds = element.getBoundingClientRect();
// NOTE getting the border should not be done like this as
// it will not work in all cases.
var border = Number(element.style.border.split("px")[0])
mouse.x = event.pageX - bounds.left - scrollX - border;
mouse.y = event.pageY - bounds.top - scrollY - border;
if(event.type === "mousedown"){
mouse["button"+event.which] = true;
} else if(event.type === "mouseup"){
mouse["button"+event.which] = false;
} else if(event.type === "mouseover"){
mouse.over = true;
} else if(event.type === "mouseout"){
mouse.over = false;
mouse.button1 = false; // turn of buttons to prevent them locking
mouse.button2 = false;
mouse.button3 = false;
}
event.preventDefault(); // stops default mouse behaviour.
}
var events = "mousemove,mousedown,mouseup,mouseout,mouseover".split(',');
events.forEach(eventType => element.addEventListener(eventType,mouseEvent));
mouse.remove = function(){
events.forEach(eventType => element.removeEventListener(eventType, mouseEvent));
}
return mouse;
}
var drag = {
x : 0,
y : 0,
x1 : 0,
y1 : 0,
dragging : false,
top : 0,
left : 0,
width : 0,
height : 0,
}
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
var mouse = createMouse(canvas);
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(mouse.button1){ // is button down
if(!drag.dragging){ // is dragging
drag.x = mouse.x;
drag.y = mouse.y;
drag.dragging = true;
}
drag.x1 = mouse.x;
drag.y1 = mouse.y;
drag.top = Math.min(drag.y, drag.y1);
drag.left = Math.min(drag.x, drag.x1);
drag.width = Math.abs(drag.x - drag.x1);
drag.height = Math.abs(drag.y - drag.y1);
}else{
if(drag.dragging){
drag.dragging = false;
}
}
if(drag.dragging){
ctx.strokeRect(drag.left, drag.top, drag.width, drag.height);
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border : 1px solid black;
}
Click drag to draw rectangle.<br>
<canvas id="canvas" width= "512" height = "256"></canvas>
I am trying to create a pannable image viewer which also allows magnification. If the zoom factor or the image size is such that the image no longer paints over the entire canvas then I wish to have the area of the canvas which does not contain the image painted with a specified background color.
My current implementation allows for zooming and panning but with the unwanted effect that the image leaves a tiled trail after it during a pan operation (much like the cards in windows Solitaire when you win a game). How do I clean up my canvas such that the image does not leave a trail and my background rectangle properly renders in my canvas?
To recreate the unwanted effect set magnification to some level at which you see the dark gray background show and then pan the image with the mouse (mouse down and drag).
Code snippet added below and Plnkr link for those who wish to muck about there.
http://plnkr.co/edit/Cl4T4d13AgPpaDFzhsq1
<!DOCTYPE html>
<html>
<head>
<style>
canvas{
border:solid 5px #333;
}
</style>
</head>
<body>
<button onclick="changeScale(0.10)">+</button>
<button onclick="changeScale(-0.10)">-</button>
<div id="container">
<canvas width="700" height="500" id ="canvas1"></canvas>
</div>
<script>
var canvas = document.getElementById('canvas1');
var context = canvas.getContext("2d");
var imageDimensions ={width:0,height:0};
var photo = new Image();
var isDown = false;
var startCoords = [];
var last = [0, 0];
var windowWidth = canvas.width;
var windowHeight = canvas.height;
var scale=1;
photo.addEventListener('load', eventPhotoLoaded , false);
photo.src = "http://www.html5rocks.com/static/images/cors_server_flowchart.png";
function eventPhotoLoaded(e) {
imageDimensions.width = photo.width;
imageDimensions.height = photo.height;
drawScreen();
}
function changeScale(delta){
scale += delta;
drawScreen();
}
function drawScreen(){
context.fillRect(0,0, windowWidth, windowHeight);
context.fillStyle="#333333";
context.drawImage(photo,0,0,imageDimensions.width*scale,imageDimensions.height*scale);
}
canvas.onmousedown = function(e) {
isDown = true;
startCoords = [
e.offsetX - last[0],
e.offsetY - last[1]
];
};
canvas.onmouseup = function(e) {
isDown = false;
last = [
e.offsetX - startCoords[0], // set last coordinates
e.offsetY - startCoords[1]
];
};
canvas.onmousemove = function(e)
{
if(!isDown) return;
var x = e.offsetX;
var y = e.offsetY;
context.setTransform(1, 0, 0, 1,
x - startCoords[0], y - startCoords[1]);
drawScreen();
}
</script>
</body>
</html>
You need to reset the transform.
Add context.setTransform(1,0,0,1,0,0); just before you clear the canvas and that will fix your problem. It sets the current transform to the default value. Then befor the image is draw set the transform for the image.
UPDATE:
When interacting with user input such as mouse or touch events it should be handled independently of rendering. The rendering will fire only once per frame and make visual changes for any mouse changes that happened during the previous refresh interval. No rendering is done if not needed.
Dont use save and restore if you don't need to.
var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext("2d");
var photo = new Image();
var mouse = {}
mouse.lastY = mouse.lastX = mouse.y = mouse.x = 0;
mouse.down = false;
var changed = true;
var scale = 1;
var imageX = 0;
var imageY = 0;
photo.src = "http://www.html5rocks.com/static/images/cors_server_flowchart.png";
function changeScale(delta){
scale += delta;
changed = true;
}
// Turns mouse button of when moving out to prevent mouse button locking if you have other mouse event handlers.
function mouseEvents(event){ // do it all in one function
if(event.type === "mouseup" || event.type === "mouseout"){
mouse.down = false;
changed = true;
}else
if(event.type === "mousedown"){
mouse.down = true;
}
mouse.x = event.offsetX;
mouse.y = event.offsetY;
if(mouse.down) {
changed = true;
}
}
canvas.addEventListener("mousemove",mouseEvents);
canvas.addEventListener("mouseup",mouseEvents);
canvas.addEventListener("mouseout",mouseEvents);
canvas.addEventListener("mousedown",mouseEvents);
function update(){
requestAnimationFrame(update);
if(photo.complete && changed){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle="#333";
ctx.fillRect(0,0, canvas.width, canvas.height);
if(mouse.down){
imageX += mouse.x - mouse.lastX;
imageY += mouse.y - mouse.lastY;
}
ctx.setTransform(scale, 0, 0, scale, imageX,imageY);
ctx.drawImage(photo,0,0);
changed = false;
}
mouse.lastX = mouse.x
mouse.lastY = mouse.y
}
requestAnimationFrame(update);
canvas{
border:solid 5px #333;
}
<button onclick="changeScale(0.10)">+</button><button onclick="changeScale(-0.10)">-</button>
<canvas width="700" height="500" id ="canvas1"></canvas>
Nice Code ;)
You are seeing the 'tiled' effect in your demonstration because you are painting the scaled image to the canvas on top of itself each time the drawScreen() function is called while dragging. You can rectify this in two simple steps.
First, you need to clear the canvas between calls to drawScreen() and second, you need to use the canvas context.save() and context.restore() methods to cleanly reset the canvas transform matrix between calls to drawScreen().
Given your code as is stands:
Create a function to clear the canvas. e.g.
function clearCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
In the canavs.onmousemove() function, call clearCanvas() and invoke context.save() before redefining the transform matrix...
canvas.onmousemove = function(e) {
if(!isDown) return;
var x = e.offsetX;
var y = e.offsetY;
/* !!! */
clearCanvas();
context.save();
context.setTransform(
1, 0, 0, 1,
x - startCoords[0], y - startCoords[1]
);
drawScreen();
}
... then conditionally invoke context.restore() at the end of drawScreen() ...
function drawScreen() {
context.fillRect(0,0, windowWidth, windowHeight);
context.fillStyle="#333333";
context.drawImage(photo,0,0,imageDimensions.width*scale,imageDimensions.height*scale);
/* !!! */
if (isDown) context.restore();
}
Additionally, you may want to call clearCanvas() before rescaling the image, and the canvas background could be styled with CSS rather than .fillRect() (in drawScreen()) - which could give a performance gain on low spec devices.
Edited in light of comments from Blindman67 below
See Also
Canvas.context.save : https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
Canvas.context.restore : https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/restore
requestAnimationFrame : https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
Paul Irish, requestAnimationFrame polyfill : http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
Call context.save to save the transformation matrix before you call context.fillRect.
Then whenever you need to draw your image, call context.restore to restore the matrix.
For example:
function drawScreen(){
context.save();
context.fillStyle="#333333";
context.fillRect(0,0, windowWidth, windowHeight);
context.restore();
context.drawImage(photo,0,0,imageDimensions.width*scale,imageDimensions.height*scale);
}
Also, to further optimize, you only need to set fillStyle once until you change the size of canvas.
I'm trying to build a canvas that i can drag using mouse movement. And I'm doing something wrong that i cannot understand cause seems to work at first and then there is like an incremental error that make the canvas move too fast.
Considering the following code,
window.onload = function() {
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
function draw() {
context.fillRect(25, 25, 100, 100);
}
function clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
var drag = false;
var dragStart;
var dragEnd;
draw()
canvas.addEventListener('mousedown', function(event) {
dragStart = {
x: event.pageX - canvas.offsetLeft,
y: event.pageY - canvas.offsetTop
}
drag = true;
})
canvas.addEventListener('mousemove', function(event) {
if (drag) {
dragEnd = {
x: event.pageX - canvas.offsetLeft,
y: event.pageY - canvas.offsetTop
}
context.translate(dragEnd.x - dragStart.x, dragEnd.y - dragStart.y);
clear()
draw()
}
})
}
live example on Plunker https://plnkr.co/edit/j8QCxwDzXJZN2DKszKwm.
Can someone help me to understand what piece I'm missing?
The problem your code has is that each time you move the rectangle blablabla px relative to the dragStart position, the translate() method is not based on dragStart position, but your current position.
To fix this, you should add the following after calling the translate method:
dragStart = dragEnd;
So that your position is also based on current mouse position.
I'm playing a bit with AMI Medical Imaging (AMI) JS ToolKit. Is there a way to move the windowing to a mouse event like right click & move?
I know that it's possible to change window/level with the menus on the examples, but I would like to change the controller to do it moving the mouse.
Thanks!
To control the window/level by moving the mouse you will have to listen to the mousemouve event then update the stackHelper -> slice -> windowWidth/Center as you wish.
You could enable window/level if the user press shift:
var drag = {
x: null,
y: null
}
var shiftDown = false;
function onKeyPressed(event){
shiftDown = event.shiftKey;
if(!shiftDown){
drag.x = null;
drag.y = null;
}
}
container.addEventListener('keydown', onKeyPressed);
container.addEventListener('keyup', onKeyPressed);
Then update the window/level on mouse move:
function onMouseMove(event){
if(!shiftDown || !stack || !stackHelper){
return;
}
if(drag.x === null){
drag.x = event.offsetX;
drag.y = event.offsetY;
}
var threshold = 15;
var dynamicRange = stack.minMax[1] - stack.minMax[0];
dynamicRange /= container.clientWidth;
if(Math.abs(event.offsetX - drag.x) > threshold){
// window width
stackHelper.slice.windowWidth += dynamicRange * (event.offsetX - drag.x);
drag.x = event.offsetX;
}
if(Math.abs(event.offsetY - drag.y) > threshold){
// window center
stackHelper.slice.windowCenter -= dynamicRange * (event.offsetY - drag.y);
drag.y = event.offsetY;
}
}
container.addEventListener('mousemove', onMouseMove);
See a live demo at (shift + mouse move to control the window level):
http://jsfiddle.net/vabL3qo0/41/
I have been using RaphealJS to create a vector drawing tool, I have all the drawing completed and working
my issues comes in when I resize the browser window and try to draw the mouse pointer is off from the location that is being drawn.
I use the mouse move event on the browser and draw lines , Like so
$(document).mousemove(function(e){
if (IE) {
var dh = $("#details").height();
var dw = $("#details").width();
xx = e.offsetX;
yy = e.offsetY;
} else {
var offset = $("#workcanvas").offset();
xx = e.pageX - offset.left;
yy = e.pageY - offset.top;
}
if (lineObject != null) {
lineObject.updateEnd(xx, yy);
} else {
lineObject = Line(xx, yy, xx, yy, MasterCanvas);
}
});
I create my canvas and background image
var MasterCanvas = Raphael($("#workcanvas").attr("id"));
var MasterBGImage = MasterCanvas.image(imgPath, 0, 0, $("#workcanvas").width(),$("#workcanvas").height());
MasterCanvas.setViewBox(0, 0, $("#workcanvas").width(), $("#workcanvas").height(), true);
and in my window resize event I tried this
MasterCanvas.setSize($("#workcanvas").width(), $("#workcanvas").height());
Now I have beat my head against this for a few days to no avail. Please note: I can the drawing function work, and as long as the window does not resize every thing is great but when the page resizes the drawing point is off.
Just in case anyone else has this problem, it turns out to be a viewBox problem, I had to calculate the mouse position based on the viewBox coordinates not the screen so my original code becomes:
$(document).mousemove(function(e){
var uupos = MasterCanvas.canvas.createSVGPoint();
uupos.x = e.clientX;
uupos.y = e.clientY;
var ctm = MasterCanvas.canvas.getScreenCTM();
if (ctm = ctm.inverse())
uupos = uupos.matrixTransform(ctm);
x = uupos.x;
y = uupos.y;
if (lineObject != null) {
lineObject.updateEnd(x, y);
} else {
lineObject = Line(x, y, x, y, MasterCanvas);
}
});
Edit:
Looks like this solution is SVG only though and it does not work in IE8 which is a requirement for me - any ideas.
Is there something like viewBox coordinates in VML