Add onclick and onmouseover to canvas element

I want to add an onclick, onmouseover and an onmouseout events to individual shapes in a canvas element.

I have tried doing this with SVG in different ways and found no single method will work in all the major browsers.

Maybe, is there a simple way to add an onclick and probably other events to canvas shapes?

Can someone please show me how to add an onclick?

Here is my code:

canvas
{
  background:gainsboro;
  border:10px ridge green;
}
<canvas id="Canvas1"></canvas>
var c=document.getElementById("Canvas1");

var ctx=c.getContext("2d");
ctx.fillStyle="blue";
ctx.fillRect(10,10,60,60);

var ctx=c.getContext("2d");
ctx.fillStyle="red";
ctx.fillRect(80,60,60,60);

// these need an onclick to fire them up. How do I add the onclick
function blue()
{
  alert("hello from blue square")
}

function red()
{
  alert("hello from red square")
}

Answers:

Answer

Here is a barebones framework for adding events to individual canvas shapes

Here's a preview: http://jsfiddle.net/m1erickson/sAFku/

Unlike SVG, after you draw a shape to canvas, there is no way to identify that shape.

On canvas, there are no individual shapes, there is just a canvas full of pixels.

To be able to identity and “use” any individual canvas shape, you need to remember all basic properties of that shape.

Here are the properties necessary to identify a rectangle:

  • x-position,
  • y-position,
  • width,
  • height.

You will also want to remember some basic styling properties of a rectangle:

  • fillcolor,
  • strokecolor,
  • strokewidth.

So here is how to create a rectangle “class” object that remembers all of it’s own basic properties.

If you're not familiar with the term "class", think of it as a "cookie-cutter" that we can use to define a shape.

Then we can use the "cookie-cutter" class to create multiple copies of that shape.

Even better ... classes are flexible enough to let us modify the basic properties of each copy that we make.

For rectangles, we can use our one class to make many, many rectangles of different widths, heights, colors and locations.

The key here is that we create classes because classes are Very Flexible and Reusable!

Here is our rect class that "remembers" all the basic info about any custom rectangle.

// the rect class 

function rect(id,x,y,width,height,fill,stroke,strokewidth) {
    this.x=x;
    this.y=y;
    this.id=id;
    this.width=width;
    this.height=height;
    this.fill=fill||"gray";
    this.stroke=stroke||"skyblue";
    this.strokewidth=strokewidth||2;
}

We can reuse this class to create as many new rectangles as we need...And we can assign different properties to our new rectangles to meet our needs for variety.

When you create an actual rectangle (by filling in it's properties), every "cookie-cutter" copy of our class has its own private set of properties.

When we use a "cookie-cutter" class to create 1+ actual rectangles to draw on the canvas, the resulting real rectangles are called "objects".

Here we create 3 real rectangle objects from our 1 class. We have assigned each real object different width, height and colors.

var myRedRect = new rect("Red-Rectangle",15,35,65,60,"red","black",3);

var myGreenRect = new rect("Green-Rectangle",115,55,50,50,"green","black",3);

var myBlueRect = new rect("Blue-Rectangle",215,95,25,20,"blue","black",3);

Now let’s give our class the ability to draw itself on the canvas by adding a draw() function. This is where we put the canvas context drawing commands and styling commands.

rect.prototype.draw(){
    ctx.save();
    ctx.beginPath();
    ctx.fillStyle=this.fill;
    ctx.strokeStyle=this.stroke;
    ctx.lineWidth=this.strokewidth;
    ctx.rect(x,y,this.width,this.height);
    ctx.stroke();
    ctx.fill();
    ctx.restore();
}

Here’s how to use the draw() function to draw rectangles on the canvas. Notice that we have 2 rectangle objects and we must execute .draw() on both of them for 2 rects to show on the canvas.

var myRedRect = new rect("Red-Rectangle",15,35,65,60,"red","black",3);
myRedRect.draw();

var myBlueRect = new rect("Blue-Rectangle",125,85,100,100,"blue","orange",3);
myBlueRect.draw();

Now give the rect class the ability to let us know if a point (mouse) is inside that rect. When the user generates mouse events, we will use this isPointInside() function to test if the mouse is currently inside our rect.

// accept a point (mouseposition) and report if it’s inside the rect

rect.prototype.isPointInside = function(x,y){
    return( x>=this.x 
            && x<=this.x+this.width
            && y>=this.y
            && y<=this.y+this.height);
}

Finally we can tie our rect class into the normal browser mouse event system.

We ask jQuery to listen for mouse clicks on the canvas. Then we feed that mouse position to the rect object. We use the rect's isPointInside() to report back if the click was inside the rect.

// listen for click events and trigger handleMouseDown
$("#canvas").click(handleMouseDown);

// calc the mouseclick position and test if it's inside the rect
function handleMouseDown(e){

    // calculate the mouse click position
    mouseX=parseInt(e.clientX-offsetX);
    mouseY=parseInt(e.clientY-offsetY);

    // test myRedRect to see if the click was inside
    if(myRedRect.isPointInside(mouseX,mouseY)){

        // we (finally!) get to execute your code!
        alert("Hello from the "+myRedRect.id);
    }
}

// These are the canvas offsets used in handleMouseDown (or any mouseevent handler)
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;

Well...that's how you "remember" canvas shapes & how you execute the code in your question!

alert("hello from blue square")

That’s a barebones “class” that creates various rectangles and reports mouseclicks.

You can use this template as a starting point to listen for all mouse-events on all kinds of canvas shapes.

Almost all canvas shapes are either rectangular or circular.

Rectangular Canvas Elements

  • Rectangle
  • Image
  • Text
  • Line (yes!)

Circular Canvas Elements

  • Circle
  • Arc
  • Regular Polygon (yes!)

Irregular Canvas Elements

  • Curves (Cubic & Quad Beziers)
  • Path

The isPointInside() would look like this for a circle:

// check for point inside a circlular shape
circle.prototype.isPointInside = function(x,y){
    var dx = circleCenterX-x;
    var dy = circleCenterY-y;
    return( dx*dx+dy*dy <= circleRadius*circleRadius );
}

Even irregularly shaped canvas elements can have isPointInside, but that usually gets complicated!

That’s it!

Here is slightly enhanced code and a Fiddle: http://jsfiddle.net/m1erickson/sAFku/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var canvasOffset=$("#canvas").offset();
    var offsetX=canvasOffset.left;
    var offsetY=canvasOffset.top;

    //
    var rect = (function () {

        // constructor
        function rect(id,x,y,width,height,fill,stroke,strokewidth) {
            this.x=x;
            this.y=y;
            this.id=id;
            this.width=width;
            this.height=height;
            this.fill=fill||"gray";
            this.stroke=stroke||"skyblue";
            this.strokewidth=strokewidth||2;
            this.redraw(this.x,this.y);
            return(this);
        }
        //
        rect.prototype.redraw = function(x,y){
            this.x=x;
            this.y=y;
            ctx.save();
            ctx.beginPath();
            ctx.fillStyle=this.fill;
            ctx.strokeStyle=this.stroke;
            ctx.lineWidth=this.strokewidth;
            ctx.rect(x,y,this.width,this.height);
            ctx.stroke();
            ctx.fill();
            ctx.restore();
            return(this);
        }
        //
        rect.prototype.isPointInside = function(x,y){
            return( x>=this.x 
                    && x<=this.x+this.width
                    && y>=this.y
                    && y<=this.y+this.height);
        }


        return rect;
    })();


    //
    function handleMouseDown(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);

      // Put your mousedown stuff here
      var clicked="";
      for(var i=0;i<rects.length;i++){
          if(rects[i].isPointInside(mouseX,mouseY)){
              clicked+=rects[i].id+" "
          }
      }
      if(clicked.length>0){ alert("Clicked rectangles: "+clicked); }
    }


    //
    var rects=[];
    //
    rects.push(new rect("Red-Rectangle",15,35,65,60,"red","black",3));
    rects.push(new rect("Green-Rectangle",60,80,70,50,"green","black",6));
    rects.push(new rect("Blue-Rectangle",125,25,10,10,"blue","black",3));

    //
    $("#canvas").click(handleMouseDown);


}); // end $(function(){});
</script>

</head>

<body>
    <canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
Answer

In short you cannot add listeners to shapes in a canvas because the shapes aren't exposed as objects. The most straightforward way to implement this is to use a a single listener on the canvas and loop through all the objects drawn in the canvas to find the correct one.

This answer explains how to implement this using the library Raphael which also gives you a lot of other benefits.

If you don't want to use a library this is a very short example of doing it.

rects = [{ color : "blue", origin : { x : 10, y : 10 }, size : { width : 60, height: 60}},
         { color : "red", origin : { x : 80, y : 60 }, size : { width : 60, height: 60}}]

function onClick(event) {
    var length = rects.length;

    for (var i = 0; i < length; ++i) {
        var obj = rects[i];
        if (event.x > obj.x && event.x < obj.origin.x + obj.size.width &&
            event.y > obj.y && event.y < obj.origin.y + obj.size.height) {
            console.log("Object clicked: " + obj);
        }
    }

NOTE: If you have a lot of objects this approach could be a bit slow. This can be combated by using a 2D spatial data structure.

Answer

Added a more up-to-date answer: since this question was posted there are now two new techniques that can be used to detect local clicks in a canvas element:

  • Path2D: Paths can be stored on separate Path2D objects and checked using isPointInPath()
  • addHitRegion: integrates with the event system which will allow you to check the event object itself of regions

Path2D example

 var path1 = new Path2D();
 path1.rect(x1, y1, w, h);    // add sub-path to Path2D object

 var path2 = new Path2D();
 path2.rect(x2, y2, w, h);    // add sub-path to Path2D object

 // now we can iterate through the objects to check which path we
 // clicked without the need to rebuild each path as we did in the past
 if (ctx.isPointInPath(path1, x, y)) { ... }

Read more about Path2D here. A polyfill exists as well.

addHitRegion example

// define a region using path
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.addHitRegion({id: "test"});

// now we can check in the event if the region was hit by doing:
canvas.addEventListener("mousemove", function(event){
  if(event.region) {
    // a region was hit, id can be used (see specs for options)
  }
});

Read more about addHitRegion() here.

Note that it is still a bit early, both Firefox and Chrome supports this enabled via flags, other browsers will hopefully follow suit.

Answer

The main difference between Canvas and SVG is that Canvas does not retain information about shapes drawn other than resulting changes in the pixel array.

So one option would be to recognize the shapes by the corresponding pixel color value in the mouse click handler:

function onClick(event) {
  var data = ctx.getImageData(event.x, event.y, 1, 1);
  var red = data[0];
  var green = data[1];
  var blue = data[2];
  var color = red << 16 | green << 8 | blue;

  if (color == 0x0000ff) {
    blue();
  } else if (color == 0x0ff0000) {
    red();
  }
}

If you want to track clicks for multiple objects with the same color using this approach, you'll need to slightly change the color for each shape to make it trackable.

This approach won't work when you add images from a different host because in this case the same origin policy will prevent getImageData.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.