Reputation: 5648
I'm an experienced Java programmer but am looking at some JavaScript/HTML5 stuff for the first time in about a decade. I'm completely stumped on what should be the simplest thing ever.
As an example I just wanted to draw something and add an event handler to it. I'm sure I'm doing something stupid, but I've searched all over and nothing that is suggested (e.g. the answer to this question: Add onclick property to input with JavaScript) works. I'm using Firefox 10.0.1. My code follows. You'll see several commented lines and at the end of each is a description of what (or what doesn't) happen.
What's the correct syntax here? I'm going crazy!
<html>
<body>
<canvas id="myCanvas" width="300" height="150"/>
<script language="JavaScript">
var elem = document.getElementById('myCanvas');
// elem.onClick = alert("hello world"); - displays alert without clicking
// elem.onClick = alert('hello world'); - displays alert without clicking
// elem.onClick = "alert('hello world!')"; - does nothing, even with clicking
// elem.onClick = function() { alert('hello world!'); }; - does nothing
// elem.onClick = function() { alert("hello world!"); }; - does nothing
var context = elem.getContext('2d');
context.fillStyle = '#05EFFF';
context.fillRect(0, 0, 150, 100);
</script>
</body>
Upvotes: 177
Views: 355322
Reputation: 8457
2021:
To create a trackable element in HTML5 canvas you should use the new Path2D() method.
After creating your shapes with new Path2D()
where a name is given for each one of them, listen to touch or mouse events (such as onclick
, ondblclick
, oncontextmenu
, onmousemove
or etc.) on your canvas to get the point coordinates event.offsetX
and event.offsetY
then use CanvasRenderingContext2D.isPointInPath()
or CanvasRenderingContext2D.isPointInStroke()
to precisely check if the mouse is hover your element in that event.
IsPointInPath:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Create circle
const ball = new Path2D();
ball.arc(150, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(ball);
// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
// Check whether point is inside circle
if (ctx.isPointInPath(ball, event.offsetX, event.offsetY)) {
ctx.fillStyle = 'green';
}
else {
ctx.fillStyle = 'red';
}
// Draw circle
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fill(ball);
});
<canvas id="canvas"></canvas>
IsPointInStroke:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Create ellipse
const egg = new Path2D();
egg.ellipse(150, 75, 40, 60, Math.PI * .25, 0, 2 * Math.PI);
ctx.lineWidth = 25;
ctx.strokeStyle = 'red';
ctx.fill(egg);
ctx.stroke(egg);
// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
// Check whether point is inside ellipse's stroke
if (ctx.isPointInStroke(egg, event.offsetX, event.offsetY)) {
ctx.strokeStyle = 'green';
}
else {
ctx.strokeStyle = 'red';
}
// Draw ellipse
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fill(egg);
ctx.stroke(egg);
});
<canvas id="canvas"></canvas>
Example with multiple elements:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const ball = new Path2D();
ball.arc(50, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(ball);
const balltwo = new Path2D();
balltwo.arc(200, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(balltwo);
// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
// Check whether point is inside circle
if (ctx.isPointInPath(ball, event.offsetX, event.offsetY)) {
ctx.fillStyle = 'green';
ctx.fill(ball);
}
else {
ctx.fillStyle = 'red';
ctx.fill(ball);
}
if (ctx.isPointInPath(balltwo, event.offsetX, event.offsetY)) {
ctx.fillStyle = 'blue';
ctx.fill(balltwo);
}
else {
ctx.fillStyle = 'red';
ctx.fill(balltwo);
}
});
html {cursor: crosshair;}
<canvas id="canvas"></canvas>
If you have a list of dynamic elements to be checked, you can check them in a loop, like this:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
var elementslist = []
const ball = new Path2D();
ball.arc(50, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(ball);
const balltwo = new Path2D();
balltwo.arc(150, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(balltwo);
const ballthree = new Path2D();
ballthree.arc(250, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(ballthree);
elementslist.push(ball,balltwo,ballthree)
document.getElementById("canvas").addEventListener('mousemove', function(event) {
event = event || window.event;
var ctx = document.getElementById("canvas").getContext("2d")
for (var i = elementslist.length - 1; i >= 0; i--){
if (elementslist[i] && ctx.isPointInPath(elementslist[i], event.offsetX, event.offsetY)) {
document.getElementById("canvas").style.cursor = 'pointer';
ctx.fillStyle = 'orange';
ctx.fill(elementslist[i]);
return
} else {
document.getElementById("canvas").style.cursor = 'default';
ctx.fillStyle = 'red';
for (var d = elementslist.length - 1; d >= 0; d--){
ctx.fill(elementslist[d]);
}
}
}
});
<canvas id="canvas"></canvas>
Sources:
Upvotes: 55
Reputation: 490153
When you draw to a canvas
element, you are simply drawing a bitmap in immediate mode.
The elements (shapes, lines, images) that are drawn have no representation besides the pixels they use and their colour.
Therefore, to get a click event on a canvas
element (shape), you need to capture click events on the canvas
HTML element and use some math to determine which element was clicked, provided you are storing the elements' width/height and x/y offset.
To add a click
event to your canvas
element, use...
canvas.addEventListener('click', function() { }, false);
To determine which element was clicked...
var elem = document.getElementById('myCanvas'),
elemLeft = elem.offsetLeft + elem.clientLeft,
elemTop = elem.offsetTop + elem.clientTop,
context = elem.getContext('2d'),
elements = [];
// Add event listener for `click` events.
elem.addEventListener('click', function(event) {
var x = event.pageX - elemLeft,
y = event.pageY - elemTop;
// Collision detection between clicked offset and element.
elements.forEach(function(element) {
if (y > element.top && y < element.top + element.height
&& x > element.left && x < element.left + element.width) {
alert('clicked an element');
}
});
}, false);
// Add element.
elements.push({
colour: '#05EFFF',
width: 150,
height: 100,
top: 20,
left: 15
});
// Render elements.
elements.forEach(function(element) {
context.fillStyle = element.colour;
context.fillRect(element.left, element.top, element.width, element.height);
});
This code attaches a click
event to the canvas
element, and then pushes one shape (called an element
in my code) to an elements
array. You could add as many as you wish here.
The purpose of creating an array of objects is so we can query their properties later. After all the elements have been pushed onto the array, we loop through and render each one based on their properties.
When the click
event is triggered, the code loops through the elements and determines if the click was over any of the elements in the elements
array. If so, it fires an alert()
, which could easily be modified to do something such as remove the array item, in which case you'd need a separate render function to update the canvas
.
For completeness, why your attempts didn't work...
elem.onClick = alert("hello world"); // displays alert without clicking
This is assigning the return value of alert()
to the onClick
property of elem
. It is immediately invoking the alert()
.
elem.onClick = alert('hello world'); // displays alert without clicking
In JavaScript, the '
and "
are semantically identical, the lexer probably uses ['"]
for quotes.
elem.onClick = "alert('hello world!')"; // does nothing, even with clicking
You are assigning a string to the onClick
property of elem
.
elem.onClick = function() { alert('hello world!'); }; // does nothing
JavaScript is case sensitive. The onclick
property is the archaic method of attaching event handlers. It only allows one event to be attached with the property and the event can be lost when serialising the HTML.
elem.onClick = function() { alert("hello world!"); }; // does nothing
Again, ' === "
.
Upvotes: 285
Reputation: 5199
Probably very late to the answer but I just read this while preparing for my 70-480
exam, and found this to work -
var elem = document.getElementById('myCanvas');
elem.onclick = function() { alert("hello world"); }
Notice the event as onclick
instead of onClick
.
JS Bin example.
Upvotes: 2
Reputation: 151
As another cheap alternative on somewhat static canvas, using an overlaying img element with a usemap definition is quick and dirty. Works especially well on polygon based canvas elements like a pie chart.
Upvotes: 1
Reputation: 1074
I recommand the following article : Hit Region Detection For HTML5 Canvas And How To Listen To Click Events On Canvas Shapes which goes through various situations.
However, it does not cover the addHitRegion
API, which must be the best way (using math functions and/or comparisons is quite error prone). This approach is detailed on developer.mozilla
Upvotes: 2
Reputation: 4931
Alex Answer is pretty neat but when using context rotate it can be hard to trace x,y coordinates, so I have made a Demo showing how to keep track of that.
Basically I am using this function & giving it the angle & the amount of distance traveled in that angel before drawing object.
function rotCor(angle, length){
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var newx = length*cos;
var newy = length*sin;
return {
x : newx,
y : newy
};
}
Upvotes: 0
Reputation: 53850
You can also put DOM elements, like div
on top of the canvas that would represent your canvas elements and be positioned the same way.
Now you can attach event listeners to these divs and run the necessary actions.
Upvotes: 1
Reputation: 2344
As an alternative to alex's answer:
You could use a SVG drawing instead of a Canvas drawing. There you can add events directly to the drawn DOM objects.
see for example:
Making an svg image object clickable with onclick, avoiding absolute positioning
Upvotes: 4