I use a canvas, in which I support mouse dragging by setting in Javascript:
canvas.onmousedown
canvas.onmouseup
canvas.onmousemove
This works.. I can support drag operations with the mouse.
On iOS safari browser, though, dragging with a finger does not trigger the mouse functions.
Instead, the entire webpage just scrolls up or down.
At first I thought adding ontouchmove and others, would fix this. But it does not.
How can the browser on a mobile device tell when touches are meant for the canvas, and when for the browser it self?
canvas.ontouchmove = function(ev) {
var x = ev.touches[0].clientX;
var y = ev.touches[0].clientY;
if ( dragging) {
drag(canvas, x, y);
}
}
There is touchstart, touchmove, and touchend. If you want the browser to not respond itself to touch events then you need to tell it to not respond them. You do that by using addEventListener instead of ontouchstart and passing {passive: false} as the last argument. Otherwise the browser doesn't wait for JavaScript before responding to the touch events. You then call preventDefault on the event object passed to the handler to tell the browser not to do the normal thing (scroll the window)
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.addEventListener('touchstart', handleTouchStart, {passive: false});
canvas.addEventListener('touchmove', handleTouchMove);
function handleTouchStart(e) {
e.preventDefault();
}
function handleTouchMove(e) {
const rect = canvas.getBoundingClientRect();
const cssX = e.touches[0].clientX - rect.left;
const cssY = e.touches[0].clientY - rect.top;
const pixelX = cssX * canvas.width / rect.width;
const pixelY = cssY * canvas.height / rect.height;
ctx.fillStyle = `hsl(${performance.now() % 360 | 0},100%,50%)`;
ctx.fillRect(pixelX - 15, pixelY - 15, 30, 30);
}
canvas {
border: 1px solid black;
width: 300px;
height: 150px;
}
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<h1>spacing</h1>
<canvas width="600" height="300"></canvas>
<h1>spacing1</h1>
<h1>spacing2</h1>
<h1>spacing3</h1>
<h1>spacing4</h1>
<h1>spacing5</h1>
<h1>spacing6</h1>
<h1>spacing7</h1>
<h1>spacing8</h1>
<h1>spacing9</h1>
<h1>spacing10</h1>
<h1>spacing11</h1>
<h1>spacing12</h1>
<h1>spacing13</h1>
<h1>spacing14</h1>
note the spacing is there to make sure there's enough space the window would scroll if you dragged your finger to show it doesn't scroll when you drag on the canvas. The meta tag is there so the browser, on mobile, shows a more mobile friendly scale.
Related
Currently working on image editing with some drag&drop.
Having something functional on chrome, but on Safari, dragend event returns stupid coordinates (e.clientX and e.clientY). Just tried on Firefox, and the values are now empty.
So this is supposed to return the mouse position on canvas when the user drops an image, which works great on Chrome, but on Safari clientX is way too big and clientY is negative...
sticker.addEventListener("dragend", (e) => {
let coords = new Array(
document.getElementById("canvas").getBoundingClientRect()
).map((rect) => {
console.log(e.clientX(e.clientY));
return [(e.clientX - rect.left)(e.clientY - rect.top)].join();
});
});
My solution was to keep track of the position given by the drag callback function (which doens't have the problem 'dragEnd' have) and then when 'dragEnd' is triggered I just use the last values from those temp variables.
Example:
drag (ev) {
this.last_x = ev.clientX;
this.last_y = ev.clientY;
...
},
dragEnd (ev) {
this.x = this.last_x;
this.y = this.last_y;
...
}
I hope this help.
Some browsers will indeed return fake values for this event, because it may well occur on a different document and even an other application than the one your page is running on.
This is somehow information leakage, and browsers don't like it.
An easy workaround is to instead listen to the drop event that should fire on your canvas element. In this event, it is admitted that the user gave you all permissions about the dragged content, so browsers will give you the correct coordinates.
const ctx = canvas.getContext('2d');
ctx.fillText('drop here', 20, 20);
canvas.addEventListener('drop', e => draw(e, 'red'));
canvas.addEventListener('dragover',e=>e.preventDefault());
sticker.addEventListener('dragstart', e=>{e.dataTransfer.setData('text', null)});
function draw(e, color) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.arc((e.clientX - rect.left), (e.clientY - rect.top), 5, 0, Math.PI*2);
ctx.fillStyle = color;
ctx.fill();
}
canvas {
border:1px solid;
}
<div id="sticker" draggable="true">drag me</div>
<canvas id="canvas"></canvas>
I have used the pointer lock on my canvas element, and the canvas is on full screen. I want to detect right clicks and left clicks to respond to them. Is it possible to respond to clicks in full screen and pointer lock? I already know how to use the pointer lock api and the fullscreen api, I don't want any answers explaining how to use them. Any help would be appreciated.
Based on the experiments I've done, the short answer is "it depends." Take a look at the following demo. There is a canvas scaled to be a quarter of the screen size in each dimension. When you move the cursor over it, a white circle appears on the canvas. When you left click, you'll draw a red circle to the canvas, and when you right click, you'll draw a cyan circle to the canvas. When you click the "Full screen" button, you'll activate pointer lock and enter fullscreen mode. If you press the "Esc" key, you'll exit pointer lock and fullscreen mode.
Note that you'll need to copy and paste the code into a file and load it. The demo won't run if you just click "Run code snippet."
As far as your question, there are two issues, I'm aware of:
In Chrome, both right- and left-click events are triggered even while in fullscreen/pointer lock. However, in Firefox, only left-click events are triggered; I was unable to get right-click events using any of the handlers I tried (click, mousedown, mouseup, contextmenu). When not in fullscreen/pointer lock, both left- and right-click events get triggered as expected in both browsers. If anyone has any solutions for listening to right-click events while in fullscreen/pointer lock, I'd love to hear them.
It seems that in pointer lock in both Chrome/Firefox, events no longer trickle down to elements contained in the element with pointer lock, but they continue to bubble up to parent elements. So in the demo, the canvas is inside a div. The div has pointer lock. onclick handlers are attached to the canvas, div, and document to report click events in the console. Without pointer lock, clicking on the canvas triggers onclick handlers for all three elements (canvas, div, and document). However, with pointer lock on the div, the onclick handler for the canvas never gets triggered, though the handlers for the div and the document do.
I also identified a couple other quirks to Firefox that, while not directly related to your initial question, might be helpful to folks interested in implementing this sort of thing:
When fullscreen mode is entered, Firefox will apply styles to the fullscreen element to get it to fill the screen. I was unable to get the canvas styled correctly (i.e. to take up the full screen) when it was placed full screen. Rather, I had to wrap the canvas in a div and enter full screen on the div. See the Fullscreen API documentation on MDN for more info:
if you're trying to emulate WebKit's behavior on Gecko, you need to place the element you want to present inside another element, which you'll make fullscreen instead, and use CSS rules to adjust the inner element to match the appearance you want.
In Firefox, activating fullscreen mode deactivated pointer lock. In order to get both activated, I had to first activate fullscreen mode and then activate pointer lock. However the simple two lines of code:
canvasContainer.requestFullscreen();
canvasContainer.requestPointerLock();
did not work. My understanding of what was happening is that the call to requestPointerLock got initiated before full screen mode was fully established. This led to pointer lock being activated and then quickly deactivated again. I found it necessary to wait until fullscreen mode was fully established before calling requestPointerLock(). Checking that document.mozFullScreenElement !== null seemed to be sufficient for checking that full screen mode was completely operational. The following following click handler definition worked to solve this problem for me:
document.getElementById('fullscreen_button').onclick = function(e) {
// When button is clicked, enter both full screen and pointer lock
canvasContainer.requestFullscreen();
var timeout = 2000;
var interval = window.setInterval(function() {
if (document.mozFullScreenElement !== null) {
window.clearInterval(interval);
canvasContainer.requestPointerLock();
} else if (timeout <= 0) {
addErrorMessage('Unable to establish pointer lock.');
clearTimeout(interval);
} else {
timeout -= 50;
}
}, 50);
}
This function repeatedly checks if full screen mode is established. When it is, it initiate pointer lock. If fullscreen mode can't be determined after 2 s, it times out.
I haven't done any testing in IE.
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<style>
</style>
</head>
<body>
<p id="msgs">Click 'Full screen' button below to go full screen. <br>
Click the left mouse button to draw a red circle. <br>
Click any other mouse button to draw a cyan circle. <br>
Press the 'Esc' key to exit full screen.</p>
<div id="canvas_container">
<canvas id="canvas"> </canvas>
</div>
<br>
<button id='fullscreen_button'>Full screen</button>
</body>
<script>
// Display constants
var CANVAS_BG_COLOR = 'rgb(75, 75, 75)';
var LEFT_CLICK_COLOR = 'rgb(255, 150, 150)';
var OTHER_CLICK_COLOR = 'rgb(150, 255, 255)';
var CURSOR_COLOR = 'rgb(200, 200, 200)';
var CANVAS_SCALING_FACTOR = 4; // Ratio between screen dimension and canvas dimension before going full-screen
// Store mouse position
var mouseX, mouseY;
// Setup onscreen canvas, smaller than the screen by a factor of CANVAS_SCALING_FACTOR
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = screen.width/CANVAS_SCALING_FACTOR;
canvas.height = screen.height/CANVAS_SCALING_FACTOR;
// Create an offscreen canvas that's the same as the size of the screen
var offscreenCanvas = document.createElement('canvas');
var offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.width = screen.width;
offscreenCanvas.height = screen.height;
var canvasContainer = document.getElementById('canvas_container');
// Radius of the circle drawn and of the circle cursor
var circleRadius = 12;
var cursorRadius = circleRadius/CANVAS_SCALING_FACTOR
offscreenCtx.drawCircle = ctx.drawCircle = function (x, y, color, radius) {
this.fillStyle = color;
this.beginPath();
this.arc(x, y, radius, 0, 2*Math.PI, true);
this.fill();
}
offscreenCtx.clearCanvas = function() {
this.fillStyle = CANVAS_BG_COLOR;
this.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
ctx.update = function() {
// Copy the offscreen canvas, scaling down if not in full-screen mode
this.drawImage(offscreenCanvas, 0, 0, offscreenCanvas.width, offscreenCanvas.height,
0, 0, canvas.width, canvas.height);
// Draw the cursor
this.drawCircle(mouseX, mouseY, CURSOR_COLOR, cursorRadius);
}
function pointerLockActive() {
return document.pointerLockElement===canvasContainer || document.mozPointerLockElement === canvasContainer;
}
// Perform initial canvas setup
offscreenCtx.clearCanvas();
ctx.update();
// Setup pointerlock and fullscreen API functions for cross-browser support
function addErrorMessage(msg) {
document.getElementById('msgs').innerHTML += ('<br><font color="red">' + msg + '</font>');
}
canvasContainer.requestPointerLock = canvasContainer.requestPointerLock || canvasContainer.mozRequestPointerLock;
canvasContainer.requestFullscreen = canvasContainer.webkitRequestFullscreen || canvasContainer.mozRequestFullScreen || canvasContainer.msRequestFullscreen
if (!canvasContainer.requestPointerLock) addErrorMessage('Error: Pointer lock not available');
if (!canvasContainer.requestFullscreen) addErrorMessage('Error: Full screen mode not available');
canvasContainer.addEventListener('mousemove', function(e) {
if (pointerLockActive()) {
// If in pointer lock, then cursor positions need to be updated manually;
// Normal cursor positions (e.g. e.clientX and e.clientY) don't get updated in pointer lock
mouseX += e.movementX, mouseY += e.movementY;
// Prevent the mouse from moving off-screen
mouseX = Math.min(Math.max(0, mouseX), canvas.width);
mouseY = Math.min(Math.max(0, mouseY), canvas.height);
} else {
// If pointer lock is inactive, then mouse position is just position relative to canvas offset
mouseX = (e.pageX - canvas.offsetLeft)
mouseY = (e.pageY - canvas.offsetTop)
}
ctx.update(); // Update the onscreen canvas
}, false);
// Handle entering and exiting pointer lock; pointer lock status is yoked to full screen status; both are entered and exited at the same time
document.addEventListener('pointerlockchange', function(e) {
if (!pointerLockActive()) {
console.log('Pointer lock deactivated');
canvas.width /= CANVAS_SCALING_FACTOR;
canvas.height /= CANVAS_SCALING_FACTOR
cursorRadius /= CANVAS_SCALING_FACTOR;
} else {
console.log('Pointer lock activated')
canvas.width *= CANVAS_SCALING_FACTOR;
canvas.height *= CANVAS_SCALING_FACTOR;
cursorRadius *= CANVAS_SCALING_FACTOR;
// Set the initial mouse position to be the middle of the canvas
mouseX = screen.width/2, mouseY = screen.height/2;
}
// Update the onscreen canvas
ctx.update();
});
document.getElementById('fullscreen_button').onclick = function(e) {
// When button is clicked, enter both full screen and pointer lock
canvasContainer.requestFullscreen();
var timeout = 2000;
var interval = window.setInterval(function() {
if (document.mozFullScreenElement !== null) {
window.clearInterval(interval);
canvasContainer.requestPointerLock();
} else if (timeout <= 0) {
addErrorMessage('Unable to establish pointer lock.');
clearTimeout(interval);
} else {
timeout -= 50;
}
}, 50);
}
canvasContainer.onclick = function(e) {
console.log('canvasContainer clicked');
if (pointerLockActive())
// If pointer lock is active, then use the mouseX and mouseY positions that are manually updated by the mousemove event handler
var cursorX = mouseX, cursorY = mouseY;
else
// Otherwise use the mouse positions passed in the event object
// If not in full screen mode, the cursor position has to be scaled up, because the mouse position is relative to the onscreen canvas, but we're drawing on the offscreen canvas, which is larger by a factor of fullscreenScale
var cursorX = (e.pageX - canvas.offsetLeft)*CANVAS_SCALING_FACTOR, cursorY = (e.pageY - canvas.offsetTop)*CANVAS_SCALING_FACTOR;
// If the left mouse button is clicked (e.which===1), draw a circle of one color
// If any other mouse button is clicked, draw a circle of another color
var color = e.which === 1 ? LEFT_CLICK_COLOR : OTHER_CLICK_COLOR;
offscreenCtx.drawCircle(cursorX, cursorY, color, circleRadius);
ctx.update();
};
// Detect canvas right-click events. Prevent default behavior (e.g. context menu display) and pass on to the onclick handler to do the rest of the work
canvasContainer.oncontextmenu = function(e) {
e.preventDefault();
this.onclick(e);
}
canvas.onclick = function() {
console.log('canvas clicked');
}
document.onclick = function() {
console.log('document clicked');
}
</script>
</html>
This worked for me to handle rightClick after pointer was locked.
const onMouseDown = (evt) => {
switch (evt.which) {
case 1: return handleLeftClick();
case 3: return handleRightClick();
}
};
document.body.addEventListener('mousedown', onMouseDown, true);
When I try to swipe over a canvas starting outside of it, event listener attached to one does not receive 'touchmove' event. In other words, I need to register touchmove that bagan on parent node.
Is there a way to propagate this event from parent element to canvas? Any solution including opensource jquery plugins is acceptable.
Thanks in advance!
The main problem with this approach is that the events captured on the parent element are going to propagate to child elements. A possible solution would be to listen for touch or mouse events on the parent node and then check if the finger is touching the outside elements. If so dispatch the mousemove or touchmove events on the canvas itself then capture this event on the canvas.
We will test the mouse pointer to be on the canvas area and we will dispatch the touch event only on this case. This way the event will be captured by canvas only when finger reaches canvas area.
I've created a snippet for illustration. I've provided example for mousemove events, but you can extend to touchmove events too. I didn't included the canvas event listener main part, i only consoled a simple message.
HTML:
<div class="canvas-holder">
<canvas id="canvas" width=300 height=300></canvas>
</div>
JS:
var canvasHolder = document.getElementsByClassName('canvas-holder')[0];
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var offsetX = canvas.offsetLeft;
var offsetY = canvas.offsetTop;
ctx.fillStyle = "rgba(255,0,0,1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
var canvasPosLeft = canvas.getBoundingClientRect().left;
var canvasPosTop = canvas.getBoundingClientRect().top;
var canvasPosRight = canvas.getBoundingClientRect().right;
var canvasPosBottom = canvas.getBoundingClientRect().bottom;
var event = document.createEvent('MouseEvents');
event.initMouseEvent('mousemove', true, true, window, 1, 100, 100, 100, 100);
// Listen for the event.
canvas.addEventListener('mousemove', function (e) {
// main logic here...
console.log('mouse triggered');
}, false);
canvasHolder.addEventListener('mousemove', function(e) {
e.stopPropagation();
var posX = e.clientX;
var posY = e.clientY;
if ((posX < canvasPosLeft || posX > canvasPosRight ) ||
(posY < canvasPosTop || posY > canvasPosBottom )) {
// Dispatch the event.
canvas.dispatchEvent(event);
}
});
And here is the fiddle:
Fiddle
The canvas signing works with mouse but isn't working with mobile. What am I missing?
When I use the canvas on my computer, the mouse-draw feature works well but when I open the file via mobile, the signature pad doesn't work. I have looked through my code but I can't identify the problem. Any ideas?
The HTML:
<!--The Signature Pad & Clear Button-->
<canvas id="sketchpad" width="500" height="200" style="background-color:#C4C4C4"></canvas>
<button type="button" value="Clear Sketchpad" id="clearbutton" onclick="clearCanvas(canvas,ctx);">Clear</button>
The JavaScript:
<script type="text/javascript">
// Variables for referencing the canvas and 2dcanvas context
var canvas,ctx;
// Variables to keep track of the mouse position and left-button status
var mouseX,mouseY,mouseDown=0;
// Variables to keep track of the touch position
var touchX,touchY;
// Draws a dot at a specific position on the supplied canvas name
// Parameters are: A canvas context, the x position, the y position, the size of the dot
function drawDot(ctx,x,y,size) {
// Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
r=0; g=0; b=0; a=255;
// Select a fill style
ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
// Draw a filled circle
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
// Clear the canvas context using the canvas width and height
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// Keep track of the mouse button being pressed and draw a dot at current location
function sketchpad_mouseDown() {
mouseDown=1;
drawDot(ctx,mouseX,mouseY,4);
}
// Keep track of the mouse button being released
function sketchpad_mouseUp() {
mouseDown=0;
}
// Kepp track of the mouse position and draw a dot if mouse button is currently pressed
function sketchpad_mouseMove(e) {
// Update the mouse co-ordinates when moved
getMousePos(e);
// Draw a dot if the mouse button is currently being pressed
if (mouseDown==1) {
drawDot(ctx,mouseX,mouseY,4);
}
}
// Get the current mouse position relative to the top-left of the canvas
function getMousePos(e) {
if (!e)
var e = event;
if (e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
else if (e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
}
// Draw something when a touch start is detected
function sketchpad_touchStart() {
// Update the touch co-ordinates
getTouchPos();
drawDot(ctx,touchX,touchY,4);
// Prevents an additional mousedown event being triggered
event.preventDefault();
}
// Draw something and prevent the default scrolling when touch movement is detected
function sketchpad_touchMove(e) {
// Update the touch co-ordinates
getTouchPos(e);
// During a touchmove event, unlike a mousemove event, we don't need to check if the touch is engaged, since there will always be contact with the screen by definition.
drawDot(ctx,touchX,touchY,4);
// Prevent a scrolling action as a result of this touchmove triggering.
event.preventDefault();
}
// Get the touch position relative to the top-left of the canvas
// When we get the raw values of pageX and pageY below, they take into account the scrolling on the page
// but not the position relative to our target div. We'll adjust them using "target.offsetLeft" and
// "target.offsetTop" to get the correct values in relation to the top left of the canvas.
function getTouchPos(e) {
if (!e)
var e = event;
if(e.touches) {
if (e.touches.length == 1) { // Only deal with one finger
var touch = e.touches[0]; // Get the information for finger #1
touchX=touch.pageX-touch.target.offsetLeft;
touchY=touch.pageY-touch.target.offsetTop;
}
}
}
// Set-up the canvas and add our event handlers after the page has loaded
function init() {
// Get the specific canvas element from the HTML document
canvas = document.getElementById('sketchpad');
// If the browser supports the canvas tag, get the 2d drawing context for this canvas
if (canvas.getContext)
ctx = canvas.getContext('2d');
// Check that we have a valid context to draw on/with before adding event handlers
if (ctx) {
// React to mouse events on the canvas, and mouseup on the entire document
canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
window.addEventListener('mouseup', sketchpad_mouseUp, false);
// React to touch events on the canvas
canvas.addEventListener('touchstart', sketchpad_touchStart, false);
canvas.addEventListener('touchmove', sketchpad_touchMove, false);
}
}
</script>
I have fixed this buy removing the "position:relative;" from the parent div of canvas.
The code below draws a rectangle on every mouse move after the mouse button is pressed. When the user releases the mouse it should stop drawing.
I am trying to figure out how to make sure that drawing stops if the user releases the mouse outside the canvas element.
I thought that I could accomplish this by setting onmouseup event handler on my event inside onmousedown like this:
canvas.onmousedown = function (e) {
drawing = true;
e.onmouseup = function (v) { drawing = false; }
};
but it did not work because e.onmouseup is never called. So I ended up setting window.onmouseup instead.
Questions:
why was my e.onmouseup never called?
is my final solution the best way to do it?
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<script type="text/javascript">
drawing = false;
function on_load(e) {
var canvas = document.getElementById("canvas");
canvas.onmousedown = function (e) { drawing = true; };
canvas.onmousemove = function (e) {
if (drawing)
{
var x = e.pageX - canvas.offsetLeft;
var y = e.pageY - canvas.offsetTop;
var context = canvas.getContext("2d");
context.strokeRect(x, y, 10, 10);
}
};
window.onmouseup = function (e) { drawing = false; };
}
</script>
</head>
<body onload="on_load()">
<canvas id="canvas" width="500" height="500" style="border: 1px solid black;">
</canvas>
</body>
</html>
Why was my e.onmouseup never called?
e is an Event object. It doesn't have a defined behaviour for an onmouseup property
2. Is my final solution the best way to do it?
Yes, but with some adjustments. First, consider if you really need the global variable. The same effect can be achieved without the global variable, but it might useful to keep it global for debugging purposes.
Second, your not-working code is not needed. It's better to have one always-existing event listener which only changes a harmless variable, than constructing a new event listener for each mouseup event. Besides, in your "wanted" code, the previous mouseup event is never explicitly removed.
function on_load(e) {
var drawing = false; // <-- Always declare variables
var canvas = document.getElementById("canvas");
canvas.onmousedown = function (e) { drawing = true; };
canvas.onmousemove = function (e) {
if (drawing) {
var x = e.pageX - canvas.offsetLeft;
var y = e.pageY - canvas.offsetTop;
var context = canvas.getContext("2d");
context.strokeRect(x, y, 10, 10);
}
};
window.onmouseup = function (e) { drawing = false; };
}
How can an OS guarantee anything like a balanced mouse down/up in some widget. Then if you get it working on OS/browser 'X' how do you know that OS/browser 'i' or OS/browser 'W' will all work the same way? There are a lot of reasons why you need to do something else, like even a timer. Likely the overhead of a timer would be small compared to what you want to do.
You may want to consider an onmouseout handler to stop drawing when the mouse leaves your element.