Case Study... Teddy

The aim of this game is to use the mouse to move Teddy's limbs.

This game shows:

 

Play game

Download .zip file

teddy_game.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  */
let backgroundImage = new Image();
backgroundImage.src = "images/sunshine.png";

let torsoImage = new Image();
torsoImage.src = "images/torso.png";

let headImage = new Image();
headImage.src = "images/head.png";

let leftArmUpperImage = new Image();
leftArmUpperImage.src = "images/left_arm_upper.png";

let leftArmLowerImage = new Image();
leftArmLowerImage.src = "images/left_arm_lower.png";

let leftArmHandImage = new Image();
leftArmHandImage.src = "images/left_arm_hand.png";

let rightArmUpperImage = new Image();
rightArmUpperImage.src = "images/right_arm_upper.png";

let rightArmLowerImage = new Image();
rightArmLowerImage.src = "images/right_arm_lower.png";

let rightArmHandImage = new Image();
rightArmHandImage.src = "images/right_arm_hand.png";

let leftLegUpperImage = new Image();
leftLegUpperImage.src = "images/left_leg_upper.png";

let leftLegLowerImage = new Image();
leftLegLowerImage.src = "images/left_leg_lower.png";

let leftLegFootImage = new Image();
leftLegFootImage.src = "images/left_leg_foot.png";

let rightLegUpperImage = new Image();
rightLegUpperImage.src = "images/right_leg_upper.png";

let rightLegLowerImage = new Image();
rightLegLowerImage.src = "images/right_leg_lower.png";

let rightLegFootImage = new Image();
rightLegFootImage.src = "images/right_leg_foot.png";

let oldMouseX = 0; // oldMouseX and oldMouseY hold the previous mouse position. This is needed to calculate the
let oldMouseY = 0; // direction that the mouse is moving in, so that we can rotate around a body part's centre of rotation

const BACKGROUND = 0;
const TEDDY1 = 1;
const TEDDY2 = 2;

let FIRST_TEDDY = TEDDY1;
let LAST_TEDDY = TEDDY2;
/******************* 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[BACKGROUND] = new StaticImage(backgroundImage, 0, 0, canvas.width, canvas.height);
    gameObjects[TEDDY1] = new Teddy(160, 250, 0.7, torsoImage, headImage, leftArmUpperImage, leftArmLowerImage, leftArmHandImage, rightArmUpperImage, rightArmLowerImage, rightArmHandImage, leftLegUpperImage, leftLegLowerImage, leftLegFootImage, rightLegUpperImage, rightLegLowerImage, rightLegFootImage);
    gameObjects[TEDDY2] = new Teddy(400, 320, 0.4, torsoImage, headImage, leftArmUpperImage, leftArmLowerImage, leftArmHandImage, rightArmUpperImage, rightArmLowerImage, rightArmHandImage, leftLegUpperImage, leftLegLowerImage, leftLegFootImage, rightLegUpperImage, rightLegLowerImage, rightLegFootImage);

    /* show hotspots on Teddy */
    /* Teddy hotspots can be draged. This will allow us to use the mouse to move the Teddy body parts */ 
    for (let teddy = FIRST_TEDDY; teddy <= LAST_TEDDY; teddy++)
    {
        if (gameObjects[teddy] !== undefined)
        {
            gameObjects[teddy].setHotSpotDrawState(true); 
        }
    }
    /* 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 */
    let NO_BODYPART_SELECTED = -1;
    let selectedTeddy = TEDDY1;
    let selectedBodyPart = NO_BODYPART_SELECTED;
    document.getElementById('gameCanvas').addEventListener('mousedown', function (e)
    {
        if (e.which === 1)  // left mouse button pressed
        {
            //  mouseIsDown = true;
        }
        let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
        let mouseX = e.clientX - canvasBoundingRectangle.left;
        let mouseY = e.clientY - canvasBoundingRectangle.top;

        document.body.style.cursor = "move";
        for (let i = 0; i < gameObjects[TEDDY1].getNumberOfBodyParts(); i++)
        {
            for (let teddy = FIRST_TEDDY; teddy <= LAST_TEDDY; teddy++)
            {
                if (gameObjects[teddy] !== undefined)
                {
                    if (gameObjects[teddy].getBodyPart(i).isHotSpot(mouseX, mouseY) === true)
                    {
                        selectedTeddy = teddy;
                        selectedBodyPart = i;
                    }
                }
            }
        }
        oldMouseX = mouseX;
        oldMouseY = mouseY;
    });

    const ROTATION_STEP_SIZE = 1;
    document.getElementById('gameCanvas').addEventListener('mousemove', function (e)
    {
        // only process 'mousemove' when a body part has been selected
        if (selectedBodyPart !== NO_BODYPART_SELECTED)
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;

            // rotate the selected bodyPart
            gameObjects[selectedTeddy].getBodyPart(selectedBodyPart).changeRotationValue(mouseX, mouseY, ROTATION_STEP_SIZE);
        
           // update the x and y location
           oldMouseX = mouseX;
           oldMouseY = mouseY;
        }
    });

    document.addEventListener('mouseup', function (e)
    {
        selectedBodyPart = NO_BODYPART_SELECTED;
        document.body.style.cursor = "auto";
    });
}

Teddy is made up by joining images of his 14 body parts together.

let backgroundImage = new Image();
backgroundImage.src = "images/sunshine.png";

let torsoImage = new Image();
torsoImage.src = "images/torso.png";

let headImage = new Image();
headImage.src = "images/head.png";

let leftArmUpperImage = new Image();
leftArmUpperImage.src = "images/left_arm_upper.png";

let leftArmLowerImage = new Image();
leftArmLowerImage.src = "images/left_arm_lower.png";

let leftArmHandImage = new Image();
leftArmHandImage.src = "images/left_arm_hand.png";

let rightArmUpperImage = new Image();
rightArmUpperImage.src = "images/right_arm_upper.png";

let rightArmLowerImage = new Image();
rightArmLowerImage.src = "images/right_arm_lower.png";

let rightArmHandImage = new Image();
rightArmHandImage.src = "images/right_arm_hand.png";

let leftLegUpperImage = new Image();
leftLegUpperImage.src = "images/left_leg_upper.png";

let leftLegLowerImage = new Image();
leftLegLowerImage.src = "images/left_leg_lower.png";

let leftLegFootImage = new Image();
leftLegFootImage.src = "images/left_leg_foot.png";

let rightLegUpperImage = new Image();
rightLegUpperImage.src = "images/right_leg_upper.png";

let rightLegLowerImage = new Image();
rightLegLowerImage.src = "images/right_leg_lower.png";

let rightLegFootImage = new Image();
rightLegFootImage.src = "images/right_leg_foot.png";

We need to keep track of the last mouse, so that we can detect the direction that the mouse is moving. This is needed when we drag on a hotspot. It allows us to know what way the hotspot should rotate.

let oldMouseX = 0; // oldMouseX and oldMouseY hold the previous mouse position. This is needed to calculate the
let oldMouseY = 0; // direction that the mouse is moving in, so that we can rotate around a body part's centre of rotation

There are three gameObjects in this game.
FIRST_TEDDY and LAST_TEDDY are used to identify the position of the first and last Teddy gameObjects. Keeping track of the first and last Teddy objects will allow us to add more Teddy objects to the game, if we wish.

const BACKGROUND = 0;
const TEDDY1 = 1;
const TEDDY2 = 2;

let FIRST_TEDDY = TEDDY1;
let LAST_TEDDY = TEDDY2;

Below is an example where FIRST_TEDDY and LAST_TEDDY are used.
Here, we step through each Teddy object and enable it to show hotspots.

    /* show hotspots on Teddy */
    /* Teddy hotspots can be draged. This will allow us to use the mouse to move the Teddy body parts */ 
    for (let teddy = FIRST_TEDDY; teddy <= LAST_TEDDY; teddy++)
    {
        if (gameObjects[teddy] !== undefined)
        {
            gameObjects[teddy].setHotSpotDrawState(true); 
        }
    }

When the mouse is pressed down (inside the 'mousedown' event handler), we check all of the hotspots on each Teddy. If we find that the mouse was clicked on a hotspot, then we set selectedTeddy and selectedBodyPart to identify the Teddy and BodyPart.
When the mouse is moved (inside the 'mousemove' event handler), we check if a bodyPart was selected. If yes, then we rotate that bodyPart. The selected bodyPart is rotated (by calling its changeRotationValue() method in the code below). The rotation is based on the direction that the mouse is moving. The oldMouseX, oldMouseY, mouseX and mouseY positions will be used to calculate the direction of that the mouse is moving.

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

        document.body.style.cursor = "move";
        for (let i = 0; i < gameObjects[TEDDY1].getNumberOfBodyParts(); i++)
        {
            for (let teddy = FIRST_TEDDY; teddy <= LAST_TEDDY; teddy++)
            {
                if (gameObjects[teddy] !== undefined)
                {
                    if (gameObjects[teddy].getBodyPart(i).isHotSpot(mouseX, mouseY) === true)
                    {
                        selectedTeddy = teddy;
                        selectedBodyPart = i;
                    }
                }
            }
        }
        oldMouseX = mouseX;
        oldMouseY = mouseY;
    });

    const ROTATION_STEP_SIZE = 1;
    document.getElementById('gameCanvas').addEventListener('mousemove', function (e)
    {
        // only process 'mousemove' when a body part has been selected
        if (selectedBodyPart !== NO_BODYPART_SELECTED)
        {
            let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
            let mouseX = e.clientX - canvasBoundingRectangle.left;
            let mouseY = e.clientY - canvasBoundingRectangle.top;

            // rotate the selected bodyPart
            gameObjects[selectedTeddy].getBodyPart(selectedBodyPart).changeRotationValue(mouseX, mouseY, ROTATION_STEP_SIZE);

            // update the x and y location
            oldMouseX = mouseX;
            oldMouseY = mouseY;
        }
    });

    document.addEventListener('mouseup', function (e)
    {
        selectedBodyPart = NO_BODYPART_SELECTED;
        document.body.style.cursor = "auto";
    });
}

Teddy.js

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

class Teddy 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(torsoCentreX, torsoCentreY, scale, torsoImage, headImage, leftArmUpperImage, leftArmLowerImage, leftArmHandImage, rightArmUpperImage, rightArmLowerImage, rightArmHandImage, leftLegUpperImage, leftLegLowerImage, leftLegFootImage, rightLegUpperImage, rightLegLowerImage, rightLegFootImage, delay = 100)
    {
        super(delay); /* as this class extends from GameObject, you must always call super() */

        this.scale = scale; // the scale is needed to convert the physical image data into the required size on the canvas

        /* These variables depend on the object */
        // assign locations in the body array for each body part
        this.HEAD = 0;
        this.TORSO = 1;
        this.LEFT_ARM_UPPER = 2;
        this.LEFT_ARM_LOWER = 3;
        this.LEFT_ARM_HAND = 4;
        this.RIGHT_ARM_UPPER = 5;
        this.RIGHT_ARM_LOWER = 6;
        this.RIGHT_ARM_HAND = 7;
        this.LEFT_LEG_UPPER = 8;
        this.LEFT_LEG_LOWER = 9;
        this.LEFT_LEG_FOOT = 10;
        this.RIGHT_LEG_UPPER = 11;
        this.RIGHT_LEG_LOWER = 12;
        this.RIGHT_LEG_FOOT = 13;
        this.NUMBER_OF_BODY_PARTS = 14;

        this.bodyPart = [];

        //  TeddyBodyPart(name, scale, sourceImage, width, height, thetha, centreOfRotationX, centreOfRotationY, hotSpotX, hotSpotY, hotSpotRadius)        
        this.bodyPart[this.TORSO] = new TeddyBodyPart(scale, torsoImage, 200, 200, 0.0, 100, 150, 100, 75, 75);
        this.bodyPart[this.HEAD] = new TeddyBodyPart(scale, headImage, 100, 100, 0.0, 50, 80, 50, 30, 40);

        this.bodyPart[this.LEFT_ARM_UPPER] = new TeddyBodyPart(scale, leftArmUpperImage, 100, 50, Math.PI / 3, 0, 25, 70, 25, 30);
        this.bodyPart[this.LEFT_ARM_LOWER] = new TeddyBodyPart(scale, leftArmLowerImage, 100, 50, Math.PI / 3, 0, 25, 70, 25, 30);
        this.bodyPart[this.LEFT_ARM_HAND] = new TeddyBodyPart(scale, leftArmHandImage, 50, 50, Math.PI / 3, 5, 35, 35, 25, 30);

        this.bodyPart[this.RIGHT_ARM_UPPER] = new TeddyBodyPart(scale, rightArmUpperImage, 100, 50, Math.PI * 2.7, 0, 25, 70, 25, 30);
        this.bodyPart[this.RIGHT_ARM_LOWER] = new TeddyBodyPart(scale, rightArmLowerImage, 100, 50, Math.PI * 2.7, 0, 25, 70, 25, 30);
        this.bodyPart[this.RIGHT_ARM_HAND] = new TeddyBodyPart(scale, rightArmHandImage, 50, 50, Math.PI * 2.7, 5, 15, 35, 15, 30);

        this.bodyPart[this.LEFT_LEG_UPPER] = new TeddyBodyPart(scale, leftLegUpperImage, 100, 100, 0.0, 50, 10, 60, 50, 40);
        this.bodyPart[this.LEFT_LEG_LOWER] = new TeddyBodyPart(scale, leftLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 40);
        this.bodyPart[this.LEFT_LEG_FOOT] = new TeddyBodyPart(scale, leftLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);

        this.bodyPart[this.RIGHT_LEG_UPPER] = new TeddyBodyPart(scale, rightLegUpperImage, 100, 100, 0.0, 50, 10, 40, 50, 40);
        this.bodyPart[this.RIGHT_LEG_LOWER] = new TeddyBodyPart(scale, rightLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 40);
        this.bodyPart[this.RIGHT_LEG_FOOT] = new TeddyBodyPart(scale, rightLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);

        // set the dependencies
        // torso is dependent on the canvas.
        // place the torso in the centre of the canvas
        this.bodyPart[this.TORSO].setInitialParentXandY(torsoCentreX, torsoCentreY);

        // all other body parts are dependent on the position of their parent
        // head
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.HEAD], 100, 10);

        // left arm
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_ARM_UPPER], 180, 50);
        this.bodyPart[this.LEFT_ARM_UPPER].setChild(this.bodyPart[this.LEFT_ARM_LOWER], 80, 25);
        this.bodyPart[this.LEFT_ARM_LOWER].setChild(this.bodyPart[this.LEFT_ARM_HAND], 100, 25);

        // right arm
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_ARM_UPPER], 20, 50);
        this.bodyPart[this.RIGHT_ARM_UPPER].setChild(this.bodyPart[this.RIGHT_ARM_LOWER], 80, 25);
        this.bodyPart[this.RIGHT_ARM_LOWER].setChild(this.bodyPart[this.RIGHT_ARM_HAND], 100, 25);

        // left leg 
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_LEG_UPPER], 135, 190);
        this.bodyPart[this.LEFT_LEG_UPPER].setChild(this.bodyPart[this.LEFT_LEG_LOWER], 60, 90);
        this.bodyPart[this.LEFT_LEG_LOWER].setChild(this.bodyPart[this.LEFT_LEG_FOOT], 25, 70);

        // right leg 
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_LEG_UPPER], 70, 190);
        this.bodyPart[this.RIGHT_LEG_UPPER].setChild(this.bodyPart[this.RIGHT_LEG_LOWER], 40, 90);
        this.bodyPart[this.RIGHT_LEG_LOWER].setChild(this.bodyPart[this.RIGHT_LEG_FOOT], 25, 70);
    }

    render()
    {
        // place all of the bodyParts in the correct position
        this.bodyPart[this.TORSO].setChildrenPositions();

        // draw all of the body parts on the main game canvas
        for (let i = this.bodyPart.length - 1; i >= 0; i--)
        {
            this.bodyPart[i].render();
        }
    }

    getBodyPart(i)
    {
        return this.bodyPart[i];
    }

    setHotSpotDrawState(state)
    {
        for (let i = 0; i < this.NUMBER_OF_BODY_PARTS; i++)
        {
            this.bodyPart[i].setDrawHotSpot(state);
        }
    }
    
    getNumberOfBodyParts()
    {
        return this.NUMBER_OF_BODY_PARTS;
    }
}

The parameter 'scale' is used to set the size of a Teddy. It is sized with a sclae that is relative to the canvas width and height.
Each of the bodyParts of the Teddy are given an identifier (as shown in green below).
The bodyPart[] array is used to hold the bodyParts (as shown in red below).
The child dependencies of each bodyParts are set up using the BodyPart's setChild() method (as shown in blue below). The linking of parent and child bodyParts allows us to rotate child bodyParts whenever a parent bodyPart has been rotated.

    constructor(torsoCentreX, torsoCentreY, scale, torsoImage, headImage, leftArmUpperImage, leftArmLowerImage, leftArmHandImage, rightArmUpperImage, rightArmLowerImage, rightArmHandImage, leftLegUpperImage, leftLegLowerImage, leftLegFootImage, rightLegUpperImage, rightLegLowerImage, rightLegFootImage, delay = 100)
    {
        super(delay); /* as this class extends from GameObject, you must always call super() */

        this.scale = scale; // the scale is needed to convert the physical image data into the required size on the canvas

        /* These variables depend on the object */
        // assign locations in the body array for each body part
        this.HEAD = 0;
        this.TORSO = 1;
        this.LEFT_ARM_UPPER = 2;
        this.LEFT_ARM_LOWER = 3;
        this.LEFT_ARM_HAND = 4;
        this.RIGHT_ARM_UPPER = 5;
        this.RIGHT_ARM_LOWER = 6;
        this.RIGHT_ARM_HAND = 7;
        this.LEFT_LEG_UPPER = 8;
        this.LEFT_LEG_LOWER = 9;
        this.LEFT_LEG_FOOT = 10;
        this.RIGHT_LEG_UPPER = 11;
        this.RIGHT_LEG_LOWER = 12;
        this.RIGHT_LEG_FOOT = 13;
        this.NUMBER_OF_BODY_PARTS = 14;

        this.bodyPart = [];

        //  TeddyBodyPart(name, scale, sourceImage, width, height, thetha, centreOfRotationX, centreOfRotationY, hotSpotX, hotSpotY, hotSpotRadius)        
        this.bodyPart[this.TORSO] = new TeddyBodyPart(scale, torsoImage, 200, 200, 0.0, 100, 150, 100, 75, 75);
        this.bodyPart[this.HEAD] = new TeddyBodyPart(scale, headImage, 100, 100, 0.0, 50, 80, 50, 30, 40);

        this.bodyPart[this.LEFT_ARM_UPPER] = new TeddyBodyPart(scale, leftArmUpperImage, 100, 50, Math.PI / 3, 0, 25, 70, 25, 30);
        this.bodyPart[this.LEFT_ARM_LOWER] = new TeddyBodyPart(scale, leftArmLowerImage, 100, 50, Math.PI / 3, 0, 25, 70, 25, 30);
        this.bodyPart[this.LEFT_ARM_HAND] = new TeddyBodyPart(scale, leftArmHandImage, 50, 50, Math.PI / 3, 5, 35, 35, 25, 30);

        this.bodyPart[this.RIGHT_ARM_UPPER] = new TeddyBodyPart(scale, rightArmUpperImage, 100, 50, Math.PI * 2.7, 0, 25, 70, 25, 30);
        this.bodyPart[this.RIGHT_ARM_LOWER] = new TeddyBodyPart(scale, rightArmLowerImage, 100, 50, Math.PI * 2.7, 0, 25, 70, 25, 30);
        this.bodyPart[this.RIGHT_ARM_HAND] = new TeddyBodyPart(scale, rightArmHandImage, 50, 50, Math.PI * 2.7, 5, 15, 35, 15, 30);

        this.bodyPart[this.LEFT_LEG_UPPER] = new TeddyBodyPart(scale, leftLegUpperImage, 100, 100, 0.0, 50, 10, 60, 50, 40);
        this.bodyPart[this.LEFT_LEG_LOWER] = new TeddyBodyPart(scale, leftLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 40);
        this.bodyPart[this.LEFT_LEG_FOOT] = new TeddyBodyPart(scale, leftLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);

        this.bodyPart[this.RIGHT_LEG_UPPER] = new TeddyBodyPart(scale, rightLegUpperImage, 100, 100, 0.0, 50, 10, 40, 50, 40);
        this.bodyPart[this.RIGHT_LEG_LOWER] = new TeddyBodyPart(scale, rightLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 40);
        this.bodyPart[this.RIGHT_LEG_FOOT] = new TeddyBodyPart(scale, rightLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);

        // set the dependencies
        // torso is dependent on the canvas.
        // place the torso in the centre of the canvas
        this.bodyPart[this.TORSO].setInitialParentXandY(torsoCentreX, torsoCentreY);

        // all other body parts are dependent on the position of their parent
        // head
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.HEAD], 100, 10);

        // left arm
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_ARM_UPPER], 180, 50);
        this.bodyPart[this.LEFT_ARM_UPPER].setChild(this.bodyPart[this.LEFT_ARM_LOWER], 80, 25);
        this.bodyPart[this.LEFT_ARM_LOWER].setChild(this.bodyPart[this.LEFT_ARM_HAND], 100, 25);

        // right arm
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_ARM_UPPER], 20, 50);
        this.bodyPart[this.RIGHT_ARM_UPPER].setChild(this.bodyPart[this.RIGHT_ARM_LOWER], 80, 25);
        this.bodyPart[this.RIGHT_ARM_LOWER].setChild(this.bodyPart[this.RIGHT_ARM_HAND], 100, 25);

        // left leg 
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_LEG_UPPER], 135, 190);
        this.bodyPart[this.LEFT_LEG_UPPER].setChild(this.bodyPart[this.LEFT_LEG_LOWER], 60, 90);
        this.bodyPart[this.LEFT_LEG_LOWER].setChild(this.bodyPart[this.LEFT_LEG_FOOT], 25, 70);

        // right leg 
        this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_LEG_UPPER], 70, 190);
        this.bodyPart[this.RIGHT_LEG_UPPER].setChild(this.bodyPart[this.RIGHT_LEG_LOWER], 40, 90);
        this.bodyPart[this.RIGHT_LEG_LOWER].setChild(this.bodyPart[this.RIGHT_LEG_FOOT], 25, 70);
    }

The render() method steps through the bodyParts[] array and renders each of the Teddy bodyParts.

    render()
    {
        // place all of the bodyParts in the correct position
        this.bodyPart[this.TORSO].setChildrenPositions();

        // draw all of the body parts on the main game canvas
        for (let i = this.bodyPart.length - 1; i >= 0; i--)
        {
            this.bodyPart[i].render();
        }
    }

TeddyBodyPart.js

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


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

    constructor(scale, sourceImage, width, height, radiants, centreOfRotationX, centreOfRotationY, hotSpotX, hotSpotY, hotSpotRadius)
    {
        // There needs to be one bodyPart for each seperate part of the body
        // For example, the left arm consists of 3 bodyParts: LEFT_ARM_UPPER, LEFT_ARM_LOWER and LEFT_HAND

        /* These variables depend on the object */
        this.scale = scale; // a scale of 1.0 means this whole Teddy is the size of the canvas.
        this.bodyPartImageFile = sourceImage; // the image file that contains this bodyPart

        this.width = width * scale;        // width and height of the bodyPart in canvas coordinates
        this.height = height * scale;
        this.radiants = radiants;      // the angle of rotation (in radiants)
        this.centreOfRotationX = centreOfRotationX * scale;  // the point around which rotations occur
        this.centreOfRotationY = centreOfRotationY * scale;

        this.hotSpotX = hotSpotX * scale;             // hot spot for identifying where the user can drag the mouse to 
        this.hotSpotY = hotSpotY * scale;             // rotate this bodyPart
        this.hotSpotRadius = hotSpotRadius * scale;   // size of the hotspot

        this.children = [];
        this.numberOfChildren = 0;      // used to keep track of the number of children in the this.children array above.

        this.hotSpotIsDrawn = false;  // by default, do not draw the hotspot on the screen

        /* this.offscreenBodyPartCanvasCtx will be used to hold the rotated bodyPart */
        /* When a bodyPart rotates, it will also cause its children to rotate */
        this.offscreenBodyPartCanvas = document.createElement('canvas');
        this.offscreenBodyPartCanvasCtx = this.offscreenBodyPartCanvas.getContext('2d');
        this.offscreenBodyPartCanvas.width = canvas.width;
        this.offscreenBodyPartCanvas.height = canvas.height;
    }

    render()
    {
        // draw rotated bodyPart onto its offscreen canvas     
        this.offscreenBodyPartCanvasCtx.clearRect(0, 0, canvas.width, canvas.height);
        this.rotateOffscreenCanvas(this.radiants, this.parentX, this.parentY);
        this.offscreenBodyPartCanvasCtx.drawImage(this.bodyPartImageFile, this.parentX - this.centreOfRotationX, this.parentY - this.centreOfRotationY, this.width, this.height);
        this.rotateOffscreenCanvas(-this.radiants, this.parentX, this.parentY);

        // if it is enabled, then draw the the hotspot on the offscreen canvas
        if (this.hotSpotIsDrawn === true)
        {
            this.drawHotSpotOnOffscreenCanvas();
        }

        // draw the offscreen canvas onto the main canvas
        ctx.drawImage(this.offscreenBodyPartCanvas, 0, 0, canvas.width, canvas.height);
    }

    setChildrenPositions()
    {
        // This method adjusts the postion of this bodyPart's children to take account of this bodyPart's postion.
        // To achieve the adjustment of position, this method sets the 'this.parentX', 'this.parentY' and 'this.radiant' variables of this bodyPart's children

        // update children to account for the rotation of this bodyPart 
        for (let i = 0; i < this.numberOfChildren; i++)
        {
            if (this.children[i] !== undefined)
            {
                // rotate each child to account for the rotation of this bodyPart
                this.children[i].updateRotatedParentXandY(this.parentX, this.parentY, this.radiants, this.centreOfRotationX, this.centreOfRotationY);

                // recursively rotate each child bodyPart about its own centre of rotation
                this.children[i].setChildrenPositions();
            }
        }
    }

    updateRotatedParentXandY(centreOfRotationX, centreOfRotationY, radiants, offsetX, offsetY)
    {
        // rotate this bodyPart to account for rotations of this bodyPart's parent
        this.parentX = this.originalParentX;
        this.parentY = this.originalParentY;
        this.parentX -= offsetX;
        this.parentY -= offsetY;

        let newX = this.parentX * Math.cos(radiants) - this.parentY * Math.sin(radiants);
        this.parentY = this.parentX * Math.sin(radiants) + this.parentY * Math.cos(radiants);
        this.parentX = newX;

        this.parentX += centreOfRotationX;
        this.parentY += centreOfRotationY;
    }
    
    rotateOffscreenCanvas(radiants, centreX, centreY)
    {
        // this is a helper method for the method 'rotate()'
        // rotate the offscreen canvas of this bodyPart around this the given centre of rotation    
        this.offscreenBodyPartCanvasCtx.translate(centreX, centreY);
        this.offscreenBodyPartCanvasCtx.rotate(radiants);
        this.offscreenBodyPartCanvasCtx.translate(-centreX, -centreY);
    }

    changeRotationValue(mouseX, mouseY, degrees)
    {
        // depending on the position of the mouse relative to the position of this.parentX and this.parentY, add or subtract degrees from this.radiants
        if (this.isRotatingClockwise(this.parentX, this.parentY, mouseX, mouseY) === true)
        {
            this.radiants += Math.radians(degrees);
        }
        else
        {
            this.radiants -= Math.radians(degrees);
        }
    }

    isRotatingClockwise(centerX, centerY, x, y)
    {
        // return true if the mouse is dragging in a clockwise direction, else return false
        if (Math.abs(x - oldMouseX) >= Math.abs(y - oldMouseY))
        {
            // mouse is being moved primarily along the x-axis
            if ((x < oldMouseX))  // mouse is moving to the left
            {
                if (y < centerY)     // mouse is above the image
                {
                    return false;
                }
                else   // mouse is below the image
                {
                    return true;
                }
            }
            else  // mouse is moving to the right
            {
                if (y < centerY)   // mouse is to the left of the image
                {
                    return true;
                }
                else  // mouse is to the right of the image
                {
                    return false;
                }
            }
        }
        else // mouse is being moved primarily along the y-axis
        {
            // mouse is being moved primarily along the y-axis
            if (y < oldMouseY)  // mouse is moving to the left
            {
                if (x < centerX)     // mouse is above the image
                {
                    return true;
                }
                else   // mouse is below the image
                {
                    return false;
                }
            }
            else  // mouse is moving to the right
            {
                if (x < centerX)   // mouse is to the left of the image
                {
                    return false;
                }
                else  // mouse is to the right of the image
                {
                    return true;
                }
            }
        }
    }

    isHotSpot(canvasX, canvasY)
    {
        // return true if the canvasX, canvasY is in this bodyPart's hotspot
        let hotSpotX = this.parentX - this.centreOfRotationX + this.hotSpotX;
        let hotSpotY = this.parentY - this.centreOfRotationY + this.hotSpotY;

        // rotate the hotspot around the parentX and parentY
        hotSpotX -= this.parentX;
        hotSpotY -= this.parentY;

        let newX = hotSpotX * Math.cos(this.radiants) - hotSpotY * Math.sin(this.radiants);
        hotSpotY = hotSpotX * Math.sin(this.radiants) + hotSpotY * Math.cos(this.radiants);
        hotSpotX = newX;

        hotSpotX += this.parentX;
        hotSpotY += this.parentY;

        if ((canvasX > hotSpotX - this.hotSpotRadius) && (canvasX < hotSpotX + this.hotSpotRadius) &&
                (canvasY > hotSpotY - this.hotSpotRadius) && (canvasY < hotSpotY + this.hotSpotRadius))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    drawHotSpotOnOffscreenCanvas()
    {
        // draw a bodyPart's hotspot on the offscreenBodyPartCanvas
        if (this.hotSpotIsDrawn === false)
        {
            return;
        }

        let hotSpotX = this.parentX - this.centreOfRotationX + this.hotSpotX;
        let hotSpotY = this.parentY - this.centreOfRotationY + this.hotSpotY;

        // rotate the hotspot around the parentX and parentY
        hotSpotX -= this.parentX;
        hotSpotY -= this.parentY;

        let newX = hotSpotX * Math.cos(this.radiants) - hotSpotY * Math.sin(this.radiants);
        hotSpotY = hotSpotX * Math.sin(this.radiants) + hotSpotY * Math.cos(this.radiants);
        hotSpotX = newX;

        hotSpotX += this.parentX;
        hotSpotY += this.parentY;

        // draw the hotspot
        this.offscreenBodyPartCanvasCtx.globalAlpha = 0.15;
        this.offscreenBodyPartCanvasCtx.beginPath();
        this.offscreenBodyPartCanvasCtx.fillStyle = "blue";
        this.offscreenBodyPartCanvasCtx.arc(hotSpotX, hotSpotY, this.hotSpotRadius, 0, Math.PI * 2);
        this.offscreenBodyPartCanvasCtx.fill();
        this.offscreenBodyPartCanvasCtx.closePath();
        this.offscreenBodyPartCanvasCtx.globalAlpha = 1;
    }

    setChild(child, parentX, parentY)
    {
        // set a direct child of this bodyPart
        // For example, LEFT_ARM_UPPER has one direct child, which is LEFT_ARM_LOWER
        this.children[this.numberOfChildren] = child;
        child.setInitialParentXandY(parentX * this.scale, parentY * this.scale);
        this.numberOfChildren++;
    }

    setInitialParentXandY(newParentX, newParentY)
    {
        // this method ties the parent's coordinates to the child's centreOfRotation coordinates
        // The parent's coordinates are the x,y values of the joint on the parent where
        // the child's rotation x,y coordinates will attach to
        this.parentX = newParentX;
        this.parentY = newParentY;
        this.originalParentX = this.parentX;
        this.originalParentY = this.parentY;
    }

    setDrawHotSpot(state)
    {
        // Not strictly necessary, but might be useful as a guide to showing younger users where to drag
        this.hotSpotIsDrawn = state;
    }

    getDegrees()
    {
        // return the rotation angle as degrees
        return this.radiants * 180 / Math.PI;
    }

    setRadiants(newDegrees)
    {
        // set the rotation angle as radiants
        // note that the input parameter, newDegrees, is given in degrees
        this.radiants = Math.radians(newDegrees);
    }

    setDrawHotSpot(state)
    {
        this.hotSpotIsDrawn = state;
    }
}

Teddy is made up of 14 BodyParts, which are linked together in a parent-child structure. Each BodyPart can have zero or more child BodyParts. Parents and children are linked at the child's point 'this.centreOfRotationX,this.centreOfRotationX' (as shown in red below). The parent's connecting point is provided an a parameter input to the method setInitialParentXandY(), which we shall look at further down in these notes.
Hotspots can be used to rotate BodyParts. Hopspots will be highlighted if the 'this.hotSpotIsDrawn' flag is set to true (as shown in blue below).
Each BodyPart has an offscreen canvas (as shown in green below). Rotations of a BodyPart will be implemented by rotating the offscreen canvas. The offscreen canvas is the same size as the main game canvas. This means that any rotations that occur on the offscreen canvas will be reflected in the final canvas when the offscreen canvas is drawn onto the main canvas.

    constructor(scale, sourceImage, width, height, radiants, centreOfRotationX, centreOfRotationY, hotSpotX, hotSpotY, hotSpotRadius)
    {
        // There needs to be one bodyPart for each seperate part of the body
        // For example, the left arm consists of 3 bodyParts: LEFT_ARM_UPPER, LEFT_ARM_LOWER and LEFT_HAND

        /* These variables depend on the object */
        this.scale = scale; // a scale of 1.0 means this whole Teddy is the size of the canvas.
        this.bodyPartImageFile = sourceImage; // the image file that contains this bodyPart

        this.width = width * scale;        // width and height of the bodyPart in canvas coordinates
        this.height = height * scale;
        this.radiants = radiants;      // the angle of rotation (in radiants)
        this.centreOfRotationX = centreOfRotationX * scale;  // the point around which rotations occur
        this.centreOfRotationY = centreOfRotationY * scale;

        this.hotSpotX = hotSpotX * scale;             // hot spot for identifying where the user can drag the mouse to 
        this.hotSpotY = hotSpotY * scale;             // rotate this bodyPart
        this.hotSpotRadius = hotSpotRadius * scale;   // size of the hotspot

        this.children = [];
        this.numberOfChildren = 0;      // used to keep track of the number of children in the this.children array above.

        this.hotSpotIsDrawn = false;  // by default, do not draw the hotspot on the screen

        /* this.offscreenBodyPartCanvasCtx will be used to hold the rotated bodyPart */
        /* When a bodyPart rotates, it will also cause its children to rotate */
        this.offscreenBodyPartCanvas = document.createElement('canvas');
        this.offscreenBodyPartCanvasCtx = this.offscreenBodyPartCanvas.getContext('2d');
        this.offscreenBodyPartCanvas.width = canvas.width;
        this.offscreenBodyPartCanvas.height = canvas.height;
    }

The render() method draws the rotated offscreen canvas on the main game canvas. It also draws the hotspot, if it is enabled.

    render()
    {
        // draw rotated bodyPart onto its offscreen canvas     
        this.offscreenBodyPartCanvasCtx.clearRect(0, 0, canvas.width, canvas.height);
        this.rotateOffscreenCanvas(this.radiants, this.parentX, this.parentY);
        this.offscreenBodyPartCanvasCtx.drawImage(this.bodyPartImageFile, this.parentX - this.centreOfRotationX, this.parentY - this.centreOfRotationY, this.width, this.height);
        this.rotateOffscreenCanvas(-this.radiants, this.parentX, this.parentY);

        // if it is enabled, then draw the the hotspot on the offscreen canvas
        if (this.hotSpotIsDrawn === true)
        {
            this.drawHotSpotOnOffscreenCanvas();
        }

        // draw the offscreen canvas onto the main canvas
        ctx.drawImage(this.offscreenBodyPartCanvas, 0, 0, canvas.width, canvas.height);
    }

The setChildrenPositions() method recursively adjusts all of this BodyPart's children to match this BodyPart's current position (as shown in red in the code below).

    setChildrenPositions()
    {
        // This method adjusts the postion of this bodyPart's children to take account of this bodyPart's postion.
        // To achieve the adjustment of position, this method sets the 'this.parentX', 'this.parentY' and 'this.radiant' variables of this bodyPart's children

        // update children to account for the rotation of this bodyPart 
        for (let i = 0; i < this.numberOfChildren; i++)
        {
            if (this.children[i] !== undefined)
            {
                // rotate each child to account for the rotation of this bodyPart
                this.children[i].updateRotatedParentXandY(this.parentX, this.parentY, this.radiants, this.centreOfRotationX, this.centreOfRotationY);

                // recursively rotate each child bodyPart about its own centre of rotation
                this.children[i].setChildrenPositions();
            }
        }
    }

The rotateOffscreenCanvas() method rotates the offscreen canvas.

    rotateOffscreenCanvas(radiants, centreX, centreY)
    {
        // this is a helper method for the method 'rotate()'
        // rotate the offscreen canvas of this bodyPart around this the given centre of rotation    
        this.offscreenBodyPartCanvasCtx.translate(centreX, centreY);
        this.offscreenBodyPartCanvasCtx.rotate(radiants);
        this.offscreenBodyPartCanvasCtx.translate(-centreX, -centreY);
    }

The changeRotationValue() method adds/subtracts 'degrees' based on the current mouse position on the canvas. The code is straightforward and does not need explaining.

    changeRotationValue(mouseX, mouseY, degrees)
    {
        // depending on the position of the mouse relative to the position of this.parentX and this.parentY, add or subtract degrees from this.radiants
        if (this.isRotatingClockwise(this.parentX, this.parentY, mouseX, mouseY) === true)
        {
            this.radiants += Math.radians(degrees);
        }
        else
        {
            this.radiants -= Math.radians(degrees);
        }
    }

The isRotatingClockwise() method is used to determine the direction that the mouse is being moved on the canvas. The code is straightforward and does not need explaining.

    isRotatingClockwise(centerX, centerY, x, y)
    {
        // return true if the mouse is dragging in a clockwise direction, else return false
        if (Math.abs(x - oldMouseX) >= Math.abs(y - oldMouseY))
        {
            // mouse is being moved primarily along the x-axis
            if ((x < oldMouseX))  // mouse is moving to the left
            {
                if (y < centerY)     // mouse is above the image
                {
                    return false;
                }
                else   // mouse is below the image
                {
                    return true;
                }
            }
            else  // mouse is moving to the right
            {
                if (y < centerY)   // mouse is to the left of the image
                {
                    return true;
                }
                else  // mouse is to the right of the image
                {
                    return false;
                }
            }
        }
        else // mouse is being moved primarily along the y-axis
        {
            // mouse is being moved primarily along the y-axis
            if (y < oldMouseY)  // mouse is moving to the left
            {
                if (x < centerX)     // mouse is above the image
                {
                    return true;
                }
                else   // mouse is below the image
                {
                    return false;
                }
            }
            else  // mouse is moving to the right
            {
                if (x < centerX)   // mouse is to the left of the image
                {
                    return false;
                }
                else  // mouse is to the right of the image
                {
                    return true;
                }
            }
        }
    }

The isHotSpot() method determines if an canvasX, canvasY coordinate is indide the hotspot area of a bodyPart. The location on the canvas must be rotated to account for the rotation of the offscreen canvas of this bodyPart.

    isHotSpot(canvasX, canvasY)
    {
        // return true if the canvasX, canvasY is in this bodyPart's hotspot
        let hotSpotX = this.parentX - this.centreOfRotationX + this.hotSpotX;
        let hotSpotY = this.parentY - this.centreOfRotationY + this.hotSpotY;

        // rotate the hotspot around the parentX and parentY
        hotSpotX -= this.parentX;
        hotSpotY -= this.parentY;

        let newX = hotSpotX * Math.cos(this.radiants) - hotSpotY * Math.sin(this.radiants);
        hotSpotY = hotSpotX * Math.sin(this.radiants) + hotSpotY * Math.cos(this.radiants);
        hotSpotX = newX;

        hotSpotX += this.parentX;
        hotSpotY += this.parentY;

        if ((canvasX > hotSpotX - this.hotSpotRadius) && (canvasX < hotSpotX + this.hotSpotRadius) &&
                (canvasY > hotSpotY - this.hotSpotRadius) && (canvasY < hotSpotY + this.hotSpotRadius))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

The setChild() method sets a bodyPart as being a child of this bodyPart. This is needed to allow us to adjust child bodyParts whenever we rotate their parent bodyPart.
The setInitialParentXandY() method connects a child back to its parent. It is called once for each child (as shown in red below)

    setChild(child, parentX, parentY)
    {
        // set a direct child of this bodyPart
        // For example, LEFT_ARM_UPPER has one direct child, which is LEFT_ARM_LOWER
        this.children[this.numberOfChildren] = child;
        child.setInitialParentXandY(parentX * this.scale, parentY * this.scale);
        this.numberOfChildren++;
    }
    
    setInitialParentXandY(newParentX, newParentY)
    {
        // this method ties the parent's coordinates to the child's centreOfRotation coordinates
        // The parent's coordinates are the x,y values of the joint on the parent where
        // the child's rotation x,y coordinates will attach to
        this.parentX = newParentX;
        this.parentY = newParentY;
        this.originalParentX = this.parentX;
        this.originalParentY = this.parentY;
    }

The other methods are straightforward and do not need explaining.

    setDrawHotSpot(state)
    {
        // Not strictly necessary, but might be useful as a guide to showing younger users where to drag
        this.hotSpotIsDrawn = state;
    }

    getDegrees()
    {
        // return the rotation angle as degrees
        return this.radiants * 180 / Math.PI;
    }

    setRadiants(newDegrees)
    {
        // set the rotation angle as radiants
        // note that the input parameter, newDegrees, is given in degrees
        this.radiants = Math.radians(newDegrees);
    }

    setDrawHotSpot(state)
    {
        this.hotSpotIsDrawn = state;
    }

Extend Teddy to make a DancingTeddy, as shown here. Hint: The animation is created by randomly moving some of the DancingTeddy bodyParts[] inside the DancingTeddy updateState() method.