Buttons

Buttons are very similar to static text and static images. If we want to display a button, then we can use a gameObject that does not override the GameObject class's update() method. The Button class can contain text and/or an image.

Button.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. */

const BUTTON_CENTRE = -1;
const TEXT_WIDTH = -2;
const TEXT_HEIGHT = -3;

class Button extends GameObject
{
    /* Each gameObject MUST have a constructor() and a render() method.        */
    /* If the object animates, then it must also have an updateState() method. */

    constructor(x, y, width, height, text, backgroundImage = null, fontSize = 50, font = "Times Roman", textColour = "Black", backgroundColour = "#ccc", borderColour = "#999", borderWidth = 2)
    {
        super(null); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.text = text;
        this.backgroundImage = backgroundImage;

        this.isHovering = false;

        /* set default text values */
        this.font = font;
        this.fontSize = fontSize;
        this.textColour = textColour;
        this.backgroundColour = backgroundColour;
        this.strokeStyle = borderColour;
        this.borderWidth = borderWidth;
        
        /* set the size and postition of the button */
        if (this.width === TEXT_WIDTH)
        {
            ctx.font = this.fontSize + "px " + this.font;
            this.width = ctx.measureText(this.text).width * 1.1; // make the box slightly wider than the text
        }

        if (this.height === TEXT_HEIGHT)
        {
            this.height = this.fontSize * 1.1;
        }

        if (this.x === BUTTON_CENTRE)
        {
            this.x = (canvas.width - this.width) / 2;
        }
    }

    render()
    {
        /* the ImageButton's border */
        ctx.beginPath();
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.borderWidth;
        ctx.moveTo(this.x, this.y);
        ctx.lineTo(this.x + this.width, this.y);
        ctx.lineTo(this.x + this.width, this.y + this.height);
        ctx.lineTo(this.x, this.y + this.height);
        ctx.lineTo(this.x, this.y);
        ctx.stroke();
        ctx.fillStyle = this.backgroundColour;
        ctx.fillRect(this.x, this.y, this.width, this.height);
        ctx.closePath();

        /* the button's image */
        if (this.backgroundImage !== null)
        {
            ctx.drawImage(this.backgroundImage, this.x, this.y, this.width, this.height);
        }

        /* the button's text */
        ctx.fillStyle = this.textColour;
        ctx.font = this.fontSize + "px " + this.font; // need to set the font each time, as it might have been changed elsewhere
        ctx.fillText(this.text, this.x + this.width * 0.05, this.y + this.height * 0.75);

        /* brighten the image if the mouse is hovering over it */
        if (this.isHovering)
        {
            let imageData = ctx.getImageData(this.x, this.y, this.width, this.height);
            let data = imageData.data;

            /* Manipulate the pixel data */
            for (let i = 0; i < data.length; i += 4)
            {
                data[i + 0] = data[i + 0] + 30;
                data[i + 1] = data[i + 1] + 30;
                data[i + 2] = data[i + 2] + 30;
            }

            ctx.putImageData(imageData, this.x, this.y);
        }
    }

    pointIsInsideBoundingRectangle(pointX, pointY)
    {
        if (!this.gameObjectIsDisplayed)
        {
            this.isHovering = false;
            return false;
        }
        if ((pointX > this.x) && (pointY > this.y))
        {
            if (pointX > this.x)
            {
                if ((pointX - this.x) > this.width)
                {
                    this.isHovering = false;
                    return false; // to the right of this gameObject
                }
            }

            if ((pointY - this.y) > this.height)
            {
                if (pointY > this.height)
                {
                    this.isHovering = false;
                    return false; // below this gameObject
                }
            }
        }
        else // above or to the left of this gameObject
        {
            this.isHovering = false;
            return false;
        }
        this.isHovering = true;
        return true; // inside this gameObject
    }
}

BUTTON_CENTRE is used as a flag that will set the button to be horrizontally centred.
const TEXT_WIDTH is used as a flag to indicate that the Button should be the width of the text that is on the Button.
const TEXT_HEIGHT is used as a flag to indicate that the Button should be the height of the text that is on the Button.

The render() method will draw the button's border, then its image and then its text. If the flag "this.isHovering" is set to true, then the render() method will brighten the button's image and text.

The "this.isHovering" flag matches the return value of pointIsInsideBoundingRectangle().
The pointIsInsideBoundingRectangle() method is used to detect that the mouse is hovering on the button.
The mousedown and mousemove event listners are used in the main game code to interact with the Button object, as shown in the example below.

We detect mouse over and mouse pressing of the button in the main js file, as shown in the example below.

Example hovering over and clicking a Button gameObject (Run Example).

demo_button.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.             */
/* There should always be a javaScript file with the same name as the html file. */
/* This file always holds the playGame function().                               */
/* It also holds game specific code, which will be different for each game       */





/******************** Declare game specific global data and functions *****************/
/* images must be declared as global, so that they will load before the game starts  */
const TEXT_BUTTON = 0;
const SMALL_TEXT_BUTTON = 1;
const IMAGE_BUTTON = 2;
const TEXT_AND_IMAGE_BUTTON = 3;
const MESSAGE = 4;

let cityImage = new Image;
cityImage.src = "images/city.png";
/******************* END OF Declare game specific data and functions *****************/




/* Always have a playGame() function                                     */
/* However, the content of this function will be different for each game */
function playGame()
{
    /* We need to initialise the game objects outside of the Game class */
    /* This function does this initialisation.                          */
    /* This function will:                                              */
    /* 1. create the various game game gameObjects                   */
    /* 2. store the game gameObjects in an array                     */
    /* 3. create a new Game to display the game gameObjects          */
    /* 4. start the Game                                                */


    /* Create the various gameObjects for this game. */
    /* This is game specific code. It will be different for each game, as each game will have it own gameObjects */

    gameObjects[TEXT_BUTTON] = new Button(BUTTON_CENTRE, 10, TEXT_WIDTH, TEXT_HEIGHT, "Text Button!");
    gameObjects[SMALL_TEXT_BUTTON] = new Button(BUTTON_CENTRE, 100, TEXT_WIDTH, TEXT_HEIGHT, "Small Text!", false, null, 24, "Times Roman", "white", "#6d6");
    gameObjects[IMAGE_BUTTON] = new Button(BUTTON_CENTRE, 150, 200, 150, "", false, cityImage);
    gameObjects[TEXT_AND_IMAGE_BUTTON] = new Button(100, 325, 300, 100, "Centred Text on Image!", true, cityImage, 20, "Times Roman", "white");

    /* END OF game specific code. */


    /* Always create a game that uses the gameObject array */
    let game = new CanvasGame();

    /* Always play the game */
    game.start();

    /* If they are needed, then include any game-specific mouse and keyboard listners */
    document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
    {
        if (e.which === 1)  // left mouse button
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;

            if (gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Small text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Text and image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
        }
    });

    document.getElementById("gameCanvas").addEventListener("mousemove", function (e)
    {
        if (e.which === 0) // no button selected
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;

            gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
        }
    });
}

If the left mouse button is clicked and the mouse is inside a Button object, then we fire some event, as highlighted in red.

    document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
    {
        if (e.which === 1)  // left mouse button
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;

            if (gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Small text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
            else if (gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
            {
                gameObjects[MESSAGE] = new StaticText("Text and image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman", 30, "black");
                gameObjects[MESSAGE].start();
            }
        }
    });

If the mouse hovers over a Button object and no mouse button has been pressed, then the Button object is highlighted by changing its colour. The colour change happens inside the Button object. Whenever the Button object returns true, it sets its "this.isHovering" flag to true, which in turn causes the Button's render() method to change the Button object's colour. This is highlighted in red in the code below.

 document.getElementById("gameCanvas").addEventListener("mousemove", function (e)
    {
        if (e.which === 0) // no button selected
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;
            gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
            gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
        }
    });