Typescript Mouse Input for HTML5 Games

To start out with, let's capture the mouse down event and output the x and y coordinates you click on the canvas. We are going to need to create a function that can handle the MouseEvent. We will call this function mouseDown and inside our onload function we will need to add an event listener to listen to our mouse down event by calling canvas.addEventListener().

The mouse down event is captured for the user clicking the mouse anywhere in the browser, not just in the canvas. This isn't real useful if you're writing a canvas game, because you want to know where the user is clicking only if that click is happening in the canvas. Because of this, we have to adjust the x and y coordinates that come into our mouseDown event handler by the canvas offset.

function mouseDown(event: MouseEvent): void {
   var x: number = event.x;
   var y: number = event.y;

   x -= canvas.offsetLeft;
   y -= canvas.offsetTop;

   alert('x=' + x + ' y=' + y);
}

window.onload = () => {
   canvas = <HTMLCanvasElement>document.getElementById('cnvs');
   canvas.addEventListener("mousedown", mouseDown, false);
   ctx = canvas.getContext("2d");

   gameLoop();
};

Once we have adjusted the x and y coordinate values inside our mouseDown() event, we will start off by doing nothing more than creating a little pop up alert with those values. You can swap that alert out with anything you want the game to actually do.

Adding a Draw Interface

We are going to have several different classes that we want to draw to the canvas. These objects should be put into an array that we can loop through and draw every frame. We will need to add the array, the new interface, and tweak the game loop to loop over the array.

var draw_array: Array<iDraw> = new Array<iDraw>();

function gameLoop(): void {
   requestAnimationFrame(gameLoop);
   ctx.fillStyle = "black";
   ctx.fillRect(0, 0, 1280, 720);
   for (var i:number = 0; i < draw_array.length; i++) {
      var d: iDraw = draw_array[i];
      d.draw();
   }
}

interface iDraw {
   x: number;
   y: number;
   draw();
}

Now, for any object we want to draw to the canvas, we will need to implement the iDraw interface, and add that object to the draw array.

A Simple Button Class

Let's create a little button object to do some hit detection on our mouse clicks. Our simple button is going to be a rectangle with some text in the middle of it. It's going to need to implement our iDraw interface and be added to the draw_array so that it can be drawn onto our canvas.

class cButton implements iDraw {
   public x: number;
   public y: number;
   public width: number;
   public height: number;

   private _halfWidth: number;
   private _halfHeight: number;

   public text: string;
   public fontSize: number;

   constructor(x: number, y: number, width: number, height: number, text: string, font_size: number = 32 ) {
      this.x = x;
      this.y = y;
      this.width = width;
      this.height = height;

      this._halfWidth= width / 2;
      this._halfHeight = height / 2;

      this.text = text;
      this.fontSize = font_size;
   }

   public draw = (): void => {
      ctx.save();
      ctx.beginPath();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "red";
      ctx.font = this.fontSize + "px Verdana";
      ctx.fillText(this.text, this.x, this.y);
      ctx.restore();

      ctx.save();
      ctx.lineWidth = 2;
      ctx.strokeStyle = "red";
      ctx.rect(this.x - this._halfWidth, this.y - this._halfHeight, this.width, this.height);
      ctx.stroke();
      ctx.restore();
   }
}

Right now this is just a rectangle with some text in the middle. We need to save and restore the context twice in the draw function. We don't want our button's rectangle to be filled with any color, but when you draw the text you need to set the fillStyle to red. Because of this we have to do our drawing in two parts. First we draw the text, then we restore the context and draw the rectangle.

Adding Click Functionality to the Button

Now we need to add some functionality to actually let us click our button. The first thing we need to add is functions that will listen to our mouse up and mouse down events. Those methods will receive a mouse down or mouse up event, then look to see if the point being clicked by the user is within the bounds of our button. The following code will go inside our cButton class.

public down: boolean = false;

public mouseDown = (event: MouseEvent): void => {
   var x: number = event.x - canvas.offsetLeft;
   var y: number = event.y - canvas.offsetTop;

   if (x > this.x - this._halfWidth && y > this.y - this._halfHeight &&
       x < this.x + this._halfWidth && y < this.y + this._halfHeight) {
      this.down = true;
   }
}

public mouseUp = (event: MouseEvent): void => {
   this.down = false;
}

These methods by themselves won't actually catch the mouse events. We will need to modify the button's constructor to capture that event and call the proper methods when the user clicks.

constructor(x: number, y: number, width: number, height: number, text: string, font_size: number = 32 ) {
   this.x = x;
   this.y = y;
   this.width = width;
   this.height = height;

   this._halfWidth= width / 2;
   this._halfHeight = height / 2;

   this.text = text;
   this.fontSize = font_size;

   canvas.addEventListener("mousedown", this.mouseDown, false);
   canvas.addEventListener("mouseup", this.mouseUp, false);

}

We are also going to change our draw method. As it is now, the only thing that happens when the player presses the button is the down flag gets set in the code to true. The user doesn't see this change, so from their perspective nothing is happening. We need to change the look of our button when the player has it pressed down.

public draw = (): void => {
   ctx.save();
   ctx.beginPath();
   ctx.textAlign = "center";
   ctx.textBaseline = "middle";
   ctx.fillStyle = "red";
   ctx.font = this.fontSize + "px Verdana";

   if (this.down == true) {
      ctx.globalAlpha = 0.5;
      ctx.fillText(this.text, this.x + 2, this.y + 2);
   }
   else {
      ctx.fillText(this.text, this.x, this.y);
   }
   ctx.restore();

   ctx.save();
   ctx.lineWidth = 2;
   ctx.strokeStyle = "red";

   if (this.down == true) {
      ctx.globalAlpha = 0.5;
      ctx.rect(this.x - this._halfWidth + 2, this.y - this._halfHeight + 2, this.width, this.height);
   }
   else {
      ctx.rect(this.x - this._halfWidth, this.y - this._halfHeight, this.width, this.height);
   }

   ctx.stroke();
   ctx.restore();
}

The first if else statement looks to see if the button was down and if it is, it sets the global alpha to 0.5 and moves the text down and over by 2 pixels. We do the same thing with the box further down in the code. These changes make the whole button move down and over by 2 pixels and makes the color look darker. This makes it look like the button is pressed.

Button not pressed

Button pressed

Dragging a button

Now let's do the first half of the drag and drop button. We are going to make a button that we can drag and drop anywhere on the canvas. Later we will want to put in a drop target so that there are only a few places you can drop your drag button, but for right now we will start with something a bit more basic. The first thing we need to add, is a little bit of code that tracks where the cursor's x and y coordinates are on the canvas at all times. This is going to need to be done by setting the document.onmousemove() function to a function we create.

var cursorX: number = 0;
var cursorY: number = 0;

document.onmousemove = (event: MouseEvent) => {
   cursorX = event.x;
   cursorY = event.y;

   cursorX -= canvas.offsetLeft;
   cursorY -= canvas.offsetTop;
}

Now we have two global variables (cursorX and cursorY) that track the x and y coordinates of our cursor on our canvas.

Next we need to copy and paste the cButton class, but rename it cDrag. This way we can start out having all of the rendering and generic mouse down and mouse up event handlers that we had in the previous class. The next thing we're going to do is change the draw() method. We are going to take down the if statements that change the way the button is rendered when the down flag is true, and add a new if statement that will follow the cursor's x and y coordinates while the mouse button is down.

public draw = (): void => {
   if (this.down == true) {
      this.x = cursorX;
      this.y = cursorY;
   }

   ctx.save();
   ctx.beginPath();
   ctx.textAlign = "center";
   ctx.textBaseline = "middle";
   ctx.fillStyle = "red";
   ctx.font = this.fontSize + "px Verdana";
   ctx.fillText(this.text, this.x, this.y);
   ctx.restore();

   ctx.save();
   ctx.lineWidth = 2;
   ctx.strokeStyle = "red";
   ctx.rect(this.x - this._halfWidth, this.y - this._halfHeight, this.width, this.height);
   ctx.stroke();
   ctx.restore();
}

Now you can drag the button around and drop it where ever you want.

Drag and Drop

Most of the time when you have a drag item, you want to be able to drop it on a specific target. In order to fully support drag and drop we're going to need to create a drag target. Once we have some target to drag our object to, we'll need to change the drag object so that it can only be dropped on a drop target. The drop target is not going to do a whole lot. It's going to need to implement the iDraw interface, and have values that the drag class can use to determine if the target is hit when the drag item is released. We also need to add a global array to keep track of all the drop targets.

var target_list: Array<cDropTarget> = new Array<cDropTarget>();

class cDropTarget implements iDraw {
   public x: number;
   public y: number;
   public width: number;
   public height: number;

   public halfWidth: number;
   public halfHeight: number;

   public text: string;
   public fontSize: number;

   public hidden: boolean = false;

   constructor(x: number, y: number, width: number, height: number, text: string, font_size: number = 12) {
      this.x = x;
      this.y = y;
      this.width = width;
      this.height = height;

      this.halfWidth = width / 2;
      this.halfHeight = height / 2;

      this.text = text;
      this.fontSize = font_size;
   }

   public draw = (): void => {
      if (this.hidden == true) {
         return;
      }

      ctx.save();
      ctx.beginPath();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "red";
      ctx.font = this.fontSize + "px Verdana";
      ctx.fillText(this.text, this.x, this.y);
      ctx.restore();

      ctx.save();
      ctx.lineWidth = 2;
      ctx.strokeStyle = "red";
      ctx.rect(this.x - this.halfWidth, this.y - this.halfHeight, this.width, this.height);
      ctx.stroke();
      ctx.restore();
   }

}

Now we need to modify our drag class so that when the player releases the mouse, we check to see if the button we are dragging hits one of the targets. The new version of our mouseUp method, we are going to need to loop over all the targets in our target_list array, and do some basic rectangle collision detection to see if the button we are dragging collides with one of the targets. If there is a collision we will need to hide the target, and place the button on top of it's position. If there isn't a collision, we will need to reset the position to where it was before we started dragging it.

class cDrag implements iDraw {
   public down: boolean;
   public x: number;
   public y: number;
   public width: number;
   public height: number;

   private _halfWidth: number;
   private _halfHeight: number;
   private _lastX: number;
   private _lastY: number;

   public text: string;
   public fontSize: number;

   public currentTarget: cDropTarget = null;

   constructor(x: number, y: number, width: number, height: number, text: string, font_size: number = 32) {
      this.x = x;
      this.y = y;
      this._lastX = x;
      this._lastY = y;
      this.width = width;
      this.height = height;

      this._halfWidth = width / 2;
      this._halfHeight = height / 2;

      this.text = text;
      this.fontSize = font_size;

      canvas.addEventListener("mousedown", this.mouseDown, false);
      canvas.addEventListener("mouseup", this.mouseUp, false);
   }

   public draw = (): void => {
      if (this.down == true) {
         this.x = cursorX;
         this.y = cursorY;
      }

      ctx.save();
      ctx.beginPath();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "red";
      ctx.font = this.fontSize + "px Verdana";
      ctx.fillText(this.text, this.x, this.y);
      ctx.restore();

      ctx.save();
      ctx.lineWidth = 2;
      ctx.strokeStyle = "red";
      ctx.rect(this.x - this._halfWidth, this.y - this._halfHeight, this.width, this.height);
      ctx.stroke();
      ctx.restore();
   }

   public mouseDown = (event: MouseEvent): void => {
      var x: number = event.x - canvas.offsetLeft;
      var y: number = event.y - canvas.offsetTop;

      if (x > this.x - this._halfWidth && y > this.y - this._halfHeight &&
          x < this.x + this._halfWidth && y < this.y + this._halfHeight) {
         this.down = true;

         if (this.currentTarget != null) {
            this.currentTarget.hidden = false;
         }
      }
   }

   public mouseUp = (event: MouseEvent): void => {
      var x: number = event.x - canvas.offsetLeft;
      var y: number = event.y - canvas.offsetTop;

      if (x > this.x - this._halfWidth && y > this.y - this._halfHeight &&
          x < this.x + this._halfWidth && y < this.y + this._halfHeight) {
         this.down = false;

         for (var i: number = 0; i < target_list.length; i++) {
            var temp_target: cDropTarget = target_list[i];
            console.log("text=" + temp_target.text);

            var xoverlap: boolean = false;
            var yoverlap: boolean = false;

            if (this.x <= temp_target.x) {
               if (this.x + this._halfWidth >= temp_target.x - this._halfWidth) {
                  xoverlap = true;
               }
            }
            else {
               if (temp_target.x + temp_target.halfWidth >= this.x - this._halfWidth) {
               xoverlap = true;
            }
         }

         if (this.y <= temp_target.y) {
            if (this.y + this._halfHeight >= temp_target.y - temp_target.halfHeight ) {
               yoverlap = true;
            }
         }
         else {
            if (temp_target.y + temp_target.halfHeight >= this.y - this._halfHeight) {
               yoverlap = true;
            }
         }

         if (xoverlap == true && yoverlap == true) {
            break;
         }
      }

      if (xoverlap == true && yoverlap == true) {
         this.x = temp_target.x;
         this.y = temp_target.y;
         this._lastX = this.x;
         this._lastY = this.y;
         this.currentTarget = temp_target;
         this.currentTarget.hidden = true;
      }
      else {
         if (this.currentTarget != null) {
            this.currentTarget.hidden = true;
            this.x = this._lastX = this.currentTarget.x;
            this.y = this._lastY = this.currentTarget.y;
            }
            else {
               this.x = this._lastX;
               this.y = this._lastY;
            }
         }

      }
   }

}

Give the app a try

Check out the full mouse input source code.