JavaScript: Intro to Web Game Development – Part 4: animate left paddle

This is the fourth part in the series on how to create a web browser game of Pong using JavaScript. Part 3 can be read here:

For this article, I will demonstrate how to add mouse control to the left paddle so that it moves straight up and down on the Y axis, based on mouse movement. In this step I am also:

  • assigning variables
  • creating a “drawComponents” function
  • setting up a framerate of 30 frames per second for interactive gameplay
  • implementing an event listener to move the left paddle according to the mouse position

Step 1: Create variables

For this step, I moved some numeric values that were previously hard coded, into global variables at the top of the script. For example:

var canvas;
var canvasContext;
var canvas_width;
var canvas_height;
// offsets to account for the rounded corners
var offsetA = 30;
var offsetB = 60;
// starting y position of the left paddle
var paddle1Y = 100;
// defaults for paddle thickness and height
const PADDLE_THICKNESS = 10;
const PADDLE_HEIGHT = 70;
var canvas; var canvasContext; var canvas_width; var canvas_height; // offsets to account for the rounded corners var offsetA = 30; var offsetB = 60; // starting y position of the left paddle var paddle1Y = 100; // defaults for paddle thickness and height const PADDLE_THICKNESS = 10; const PADDLE_HEIGHT = 70;
var canvas;
var canvasContext;
var canvas_width;
var canvas_height;

// offsets to account for the rounded corners
var offsetA = 30;
var offsetB = 60;

// starting y position of the left paddle
var paddle1Y = 100;

// defaults for paddle thickness and height
const PADDLE_THICKNESS = 10;
const PADDLE_HEIGHT = 70;

Step 2: Create drawComponents function

Things that were previously drawn in the window.onload function are now moved to a dedicated “drawComponents” function. For example:

function drawComponents() {
// this line blacks out the background to give the appearance of animation
canvasContext.fillStyle = 'black';
canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height);
// Left paddle
canvasContext.fillStyle = 'white';
canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT);
// Right paddle
canvasContext.fillStyle = 'white';
canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT);
// Draw the ball
canvasContext.fillStyle = 'white';
canvasContext.fillRect(200, 300, 10, 10);
// Place holder for the scores
canvasContext.font="40px monospace";
canvasContext.fillText("1", (canvas_width/2)-60, 40);
canvasContext.fillText("0", (canvas_width/2)+35, 40);
drawNet();
}
function drawComponents() { // this line blacks out the background to give the appearance of animation canvasContext.fillStyle = 'black'; canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height); // Left paddle canvasContext.fillStyle = 'white'; canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT); // Right paddle canvasContext.fillStyle = 'white'; canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT); // Draw the ball canvasContext.fillStyle = 'white'; canvasContext.fillRect(200, 300, 10, 10); // Place holder for the scores canvasContext.font="40px monospace"; canvasContext.fillText("1", (canvas_width/2)-60, 40); canvasContext.fillText("0", (canvas_width/2)+35, 40); drawNet(); }
function drawComponents() {

    // this line blacks out the background to give the appearance of animation
    canvasContext.fillStyle = 'black';
    canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height);

    // Left paddle
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Right paddle
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Draw the ball
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(200, 300, 10, 10);

    // Place holder for the scores
    canvasContext.font="40px monospace";
    canvasContext.fillText("1", (canvas_width/2)-60, 40);
    canvasContext.fillText("0", (canvas_width/2)+35, 40);

    drawNet();
}

Step 3: Create a frame rate and game loop

Inside the window.onload function, I am setting a framerate with the

setInterval()
setInterval() method, which sets up a game loop. Since this function contains the game loop, everything that happens in the game, all of the “action”, needs to occur in this loop, which is why there is a call to drawComponents() inside this block.

This setup, defining a framerate, and using it as the second argument to the setInterval() method, is the standard way to create a game loop in JavaScript.

var framesPerSecond = 30;
setInterval(
function() {
drawComponents();
}, 1000/framesPerSecond);
var framesPerSecond = 30; setInterval( function() { drawComponents(); }, 1000/framesPerSecond);
var framesPerSecond = 30;
setInterval(
    function() {
        drawComponents();
    }, 1000/framesPerSecond);

For more information about setInterval, see:
https://www.w3schools.com/jsref/met_win_setinterval.asp

Step 4: Create a mousemove EventListener

Also inside the window.onload function, I need to add an EventListener for mouse movements. This is achieved with the JavaScript method

addEventListener()
addEventListener() and its first argument is ‘mousemove’.

canvas.addEventListener('mousemove',
function(evt) {
var mousePos = calculateMousePos(evt);
paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
});
canvas.addEventListener('mousemove', function(evt) { var mousePos = calculateMousePos(evt); paddle1Y = mousePos.y - (PADDLE_HEIGHT/2); });
canvas.addEventListener('mousemove',
	function(evt) {
		var mousePos = calculateMousePos(evt);
		paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
	});

The paddle will be moving up and down on the Y axis. The line that controls this is:

paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);

PADDLE_HEIGHT/2 is the middle of the paddle vertically, which corresponds to the movement of the mouse.

For more information about addEventListener() see:
https://www.w3schools.com/jsref/met_element_addeventlistener.asp

Notice inside the function, a mousePos variable is being set by calling another function called calculateMousePos(). That function also needs to be added to the script, but not inside the window.onload function. It looks like this:

function calculateMousePos(evt) {
var rect = canvas.getBoundingClientRect();
var root = document.documentElement;
var mouseX = evt.clientX - rect.left - root.scrollLeft;
var mouseY = evt.clientY - rect.top - root.scrollTop;
return {
x:mouseX,
y:mouseY
};
}
function calculateMousePos(evt) { var rect = canvas.getBoundingClientRect(); var root = document.documentElement; var mouseX = evt.clientX - rect.left - root.scrollLeft; var mouseY = evt.clientY - rect.top - root.scrollTop; return { x:mouseX, y:mouseY }; }
function calculateMousePos(evt) {
    var rect = canvas.getBoundingClientRect();
    var root = document.documentElement;
    var mouseX = evt.clientX - rect.left - root.scrollLeft;
    var mouseY = evt.clientY - rect.top - root.scrollTop;
    return {
        x:mouseX,
        y:mouseY
    };
}

This function is crucial in working with the EventListener to enable movement of the paddle based on mouse movement. It uses a JavaScript method called

getBoundingClientRect()
getBoundingClientRect(). It also uses MouseEvent
clientX
clientX and
clientY
clientY properties, which output the coordinates of the mouse pointer.

For more information about getBoundingClientRect(), see:
https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

For more about clientX and clientY, see:
https://www.w3schools.com/jsref/event_clientx.asp

Here is what the entire script looks like so far:

var canvas;
var canvasContext;
var canvas_width;
var canvas_height;
// offsets to account for the rounded corners
var offsetA = 30;
var offsetB = 60;
// starting y position of the left paddle
var paddle1Y = 100;
// defaults for paddle thickness and height
const PADDLE_THICKNESS = 10;
const PADDLE_HEIGHT = 70;
window.onload = function() {
canvas = document.getElementById("gameCanvas");
canvasContext = canvas.getContext("2d");
canvas_width = canvas.width;
canvas_height = canvas.height;
// Create the rounded canvas play area
canvasContext.roundRect(0, 0, canvas_width, canvas_height, 40, "black");
var framesPerSecond = 30;
setInterval(
function() {
drawComponents();
}, 1000/framesPerSecond);
canvas.addEventListener('mousemove',
function(evt) {
var mousePos = calculateMousePos(evt);
paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
});
}
// Function to create rounded corners on the rectangle
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius, fill, stroke) {
// If stroke argument not provided, provide a stroke by default
if (typeof stroke == "undefined" ) {
stroke = true;
}
// If no radius is defined, then give it a default of 5
if (typeof radius === "undefined") {
radius = 5;
}
// Start of shape definition
this.beginPath();
this.moveTo(x + radius, y);
// Draw the top border line
this.lineTo(x + width - radius, y);
// Draw the top right curve
this.quadraticCurveTo(x + width, y, x + width, y + radius);
// Draw the right border line
this.lineTo(x + width, y + height - radius);
// Draw the bottom right curve
this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
// Draw the bottom border line
this.lineTo(x + radius, y + height);
// Draw the bottom left curve
this.quadraticCurveTo(x, y + height, x, y + height - radius);
// Draw the left border line
this.lineTo(x, y + radius);
// Draw the final (top left) curve
this.quadraticCurveTo(x, y, x + radius, y);
// End of shape definition
this.closePath();
// Draw the shape with a line
if (stroke) {
this.stroke();
}
// Fill in the shape with color
if (fill) {
this.fill();
}
}
function calculateMousePos(evt) {
var rect = canvas.getBoundingClientRect();
var root = document.documentElement;
var mouseX = evt.clientX - rect.left - root.scrollLeft;
var mouseY = evt.clientY - rect.top - root.scrollTop;
return {
x:mouseX,
y:mouseY
};
}
function drawComponents() {
// this line blacks out the background to give the appearance of animation
canvasContext.fillStyle = 'black';
canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height);
// Left paddle
canvasContext.fillStyle = 'white';
canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT);
// Right paddle
canvasContext.fillStyle = 'white';
canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT);
// Draw the ball
canvasContext.fillStyle = 'white';
canvasContext.fillRect(200, 300, 10, 10);
// Place holder for the scores
canvasContext.font="40px monospace";
canvasContext.fillText("1", (canvas_width/2)-60, 40);
canvasContext.fillText("0", (canvas_width/2)+35, 40);
drawNet();
}
function drawNet() {
for(var i=0; i < canvas_height; i += 30) {
canvasContext.fillStyle = 'white';
canvasContext.fillRect(canvas_width/2, i, 2, 10, 'white');
}
}
var canvas; var canvasContext; var canvas_width; var canvas_height; // offsets to account for the rounded corners var offsetA = 30; var offsetB = 60; // starting y position of the left paddle var paddle1Y = 100; // defaults for paddle thickness and height const PADDLE_THICKNESS = 10; const PADDLE_HEIGHT = 70; window.onload = function() { canvas = document.getElementById("gameCanvas"); canvasContext = canvas.getContext("2d"); canvas_width = canvas.width; canvas_height = canvas.height; // Create the rounded canvas play area canvasContext.roundRect(0, 0, canvas_width, canvas_height, 40, "black"); var framesPerSecond = 30; setInterval( function() { drawComponents(); }, 1000/framesPerSecond); canvas.addEventListener('mousemove', function(evt) { var mousePos = calculateMousePos(evt); paddle1Y = mousePos.y - (PADDLE_HEIGHT/2); }); } // Function to create rounded corners on the rectangle CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius, fill, stroke) { // If stroke argument not provided, provide a stroke by default if (typeof stroke == "undefined" ) { stroke = true; } // If no radius is defined, then give it a default of 5 if (typeof radius === "undefined") { radius = 5; } // Start of shape definition this.beginPath(); this.moveTo(x + radius, y); // Draw the top border line this.lineTo(x + width - radius, y); // Draw the top right curve this.quadraticCurveTo(x + width, y, x + width, y + radius); // Draw the right border line this.lineTo(x + width, y + height - radius); // Draw the bottom right curve this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); // Draw the bottom border line this.lineTo(x + radius, y + height); // Draw the bottom left curve this.quadraticCurveTo(x, y + height, x, y + height - radius); // Draw the left border line this.lineTo(x, y + radius); // Draw the final (top left) curve this.quadraticCurveTo(x, y, x + radius, y); // End of shape definition this.closePath(); // Draw the shape with a line if (stroke) { this.stroke(); } // Fill in the shape with color if (fill) { this.fill(); } } function calculateMousePos(evt) { var rect = canvas.getBoundingClientRect(); var root = document.documentElement; var mouseX = evt.clientX - rect.left - root.scrollLeft; var mouseY = evt.clientY - rect.top - root.scrollTop; return { x:mouseX, y:mouseY }; } function drawComponents() { // this line blacks out the background to give the appearance of animation canvasContext.fillStyle = 'black'; canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height); // Left paddle canvasContext.fillStyle = 'white'; canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT); // Right paddle canvasContext.fillStyle = 'white'; canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT); // Draw the ball canvasContext.fillStyle = 'white'; canvasContext.fillRect(200, 300, 10, 10); // Place holder for the scores canvasContext.font="40px monospace"; canvasContext.fillText("1", (canvas_width/2)-60, 40); canvasContext.fillText("0", (canvas_width/2)+35, 40); drawNet(); } function drawNet() { for(var i=0; i < canvas_height; i += 30) { canvasContext.fillStyle = 'white'; canvasContext.fillRect(canvas_width/2, i, 2, 10, 'white'); } }
var canvas;
var canvasContext;
var canvas_width;
var canvas_height;

// offsets to account for the rounded corners
var offsetA = 30;
var offsetB = 60;

// starting y position of the left paddle
var paddle1Y = 100;

// defaults for paddle thickness and height
const PADDLE_THICKNESS = 10;
const PADDLE_HEIGHT = 70;


window.onload = function() {

    canvas = document.getElementById("gameCanvas");
    canvasContext = canvas.getContext("2d");
    canvas_width = canvas.width;
    canvas_height = canvas.height;

    // Create the rounded canvas play area
    canvasContext.roundRect(0, 0, canvas_width, canvas_height, 40, "black");

    var framesPerSecond = 30;
    setInterval(
        function() {
            drawComponents();
        }, 1000/framesPerSecond);

    canvas.addEventListener('mousemove',
    	function(evt) {
    		var mousePos = calculateMousePos(evt);
    		paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
    	});

}

// Function to create rounded corners on the rectangle
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius, fill, stroke) {
    // If stroke argument not provided, provide a stroke by default
    if (typeof stroke == "undefined" ) {
        stroke = true;
    }
    // If no radius is defined, then give it a default of 5
    if (typeof radius === "undefined") {
        radius = 5;
    }

    // Start of shape definition
    this.beginPath();

    this.moveTo(x + radius, y);
    // Draw the top border line
    this.lineTo(x + width - radius, y);

    // Draw the top right curve
    this.quadraticCurveTo(x + width, y, x + width, y + radius);

    // Draw the right border line
    this.lineTo(x + width, y + height - radius);

    // Draw the bottom right curve
    this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);

    // Draw the bottom border line
    this.lineTo(x + radius, y + height);

    // Draw the bottom left curve
    this.quadraticCurveTo(x, y + height, x, y + height - radius);

    // Draw the left border line
    this.lineTo(x, y + radius);

    // Draw the final (top left) curve
    this.quadraticCurveTo(x, y, x + radius, y);

    // End of shape definition
    this.closePath();

    // Draw the shape with a line
    if (stroke) {
        this.stroke();
    }
    // Fill in the shape with color
    if (fill) {
        this.fill();
    }
}


function calculateMousePos(evt) {
    var rect = canvas.getBoundingClientRect();
    var root = document.documentElement;
    var mouseX = evt.clientX - rect.left - root.scrollLeft;
    var mouseY = evt.clientY - rect.top - root.scrollTop;
    return {
        x:mouseX,
        y:mouseY
    };
}

function drawComponents() {

    // this line blacks out the background to give the appearance of animation
    canvasContext.fillStyle = 'black';
    canvasContext.fillRect(offsetA, 0, canvas_width-offsetB, canvas_height);

    // Left paddle
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Right paddle
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Draw the ball
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(200, 300, 10, 10);

    // Place holder for the scores
    canvasContext.font="40px monospace";
    canvasContext.fillText("1", (canvas_width/2)-60, 40);
    canvasContext.fillText("0", (canvas_width/2)+35, 40);

    drawNet();
}

function drawNet() {
    for(var i=0; i < canvas_height; i += 30) {
        canvasContext.fillStyle = 'white';
        canvasContext.fillRect(canvas_width/2, i, 2, 10, 'white');
    }
}

And here is what the game looks like so far, with the left paddle responding to mouse movements: