Frogger game in JavaScript using Canvas API

Muhammad Saqib Ilyas
44 min readSep 23, 2023

--

I wrote an article on how to create a game of Frogger with HTML, CSS, and JavaScript. It used just the DOM API, and div elements. In this article, we’ll create a cooler version of the game, this time using the Canvas API.

A snapshot of the classic game of Frogger

Here’s the task breakdown for building the game

  • Create the game layout with a start lane, a finish lane, a moat, a road
  • Render some vehicles facing right, and some facing left
  • Set vehicles moving in both directions on the road at different speeds
  • Set some logs floating in the moat in both directions
  • Render a frog in the start lane, initially
  • Handle keyboard events to monitor arrow keys, and move the frog accordingly
  • Monitor collisions of the frog with vehicles
  • Monitor the frog sinking in the water
  • Have the frog float when it gets on a log
  • Declare the game won if the frog successfully reaches the finish lane

The HTML

Let’s start by setting up the page contents in HTML as follows:

<!DOCTYPE html>
<html>
<head>
<title>Frogger Game</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="game-container">
<div class="finish-line"></div>
<div class="blue-background"></div>
<div class="green-background"></div>
<div class="dark-gray-background"></div>
<div class="start-line"></div>
<canvas id="frogger-canvas" width="800" height="600"></canvas>
</div>
<script src="app.js"></script>
</body>
</html>

I’ve set up a div element for the game display with a CSS class of game-container. Inside this element, I’ve placed five more div elements. The first one is for the finishing line, the next one for the moat, the next one for the resting lane between the road and the moat, then the road, and finally the starting lane. Finally, I’ve placed a canvas element in which I’ll render the frog, the cars, and the logs. The canvas element is given a width of 800 pixels and a height of 600 pixels.

The CSS

In order to set up the background colours and the dimensions for each of the parts of the game display, we’ll define the following CSS in a file named style.css:

body {
margin: 0;
padding: 0;
}

.game-container {
width: 800px;
height: 540px;
position: relative;
margin: 0 auto;
}

.blue-background {
width: 800px;
height: 240px;
background-color: blue;
}

.green-background {
width: 800px;
height: 20px;
background-color: green;
}

.dark-gray-background {
width: 800px;
height: 240px;
background-color: darkgray;
}

.finish-line, .start-line {
width: 800px;
height: 20px;
background-color: green;
}

#frogger-canvas {
position: absolute;
top: 0;
left: 0;
z-index: 1; /* Place the canvas above the background */
}

For the body element, I’ve set padding and margin at 0, so that we can occupy the entire window’s dimensions. I’ve set the width of the game-container div element at 800 pixels and its height at 540 pixels. I’ve set position to absolute and margin to 0 auto so that the corresponding div element appears in the centre of the browser window.

While the game display has different portions (the moat, the grass, the road) I want all of these to be the same width. That’s why I’ve set width: 800px; in all these definitions (game-container, finish-line, start-line, green-background, blue-background, dark-grey-background). I want the finish line, the start line, and the grass in the middle to be 20 pixels in height, so that’s what I did in the CSS above. That leaves us 540–20x3 = 480 pixels for the moat, and the road. I divided that equally among the two, giving us a height of 240 pixels each.

We’ve positioned the canvas element coinciding with the top left corner of the game-container element, with a z-index of 1, so that whatever we display on the canvas should always be visible.

With this, when you open the page, it should look like the following:

The game display layout

The game assets

I downloaded some free frogger game assets from various websites. I got the frog image from clipartmax. I got the vehicle images from freepik. These images are sprite sheets. Don’t forget to either select sprite sheets with transparent background, or remove the background from the downloaded image.

The frog image looks like the following:

Frog sprite sheet

Each row in the above image matches to the frog performing a specific action. For example, the first row is the frog hopping when viewed from the front. If you would render the images in the front row one by one, you’ll see an animation that looks like a frog hopping along.

For the vehicles, I used the following:

The vehicles sprite sheet

Rendering the sprite sheet

Let’s now render the sprite sheet onto the web page. We’ll use the canvas API for that. With our HTML as already described, we’ll write the following JavaScript code to render the image:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img = new Image()
img.src = 'assets/frogger.png'


function display() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)
ctx2.drawImage(img, 0,0)
requestAnimationFrame(display)
}

display()

First, we are getting an object corresponding to the canvas element using document.getElementById(). Then, we obtain a 2D drawing context corresponding to that canvas element. The canvas API provides multiple drawing contexts — 2D is one of those. Before we render the image, we defined a couple of constants to store the width and height of the canvas element. Then, we created an img element and set its src attribute to the frogger sprite sheet image. Note that I put the image in an assets sub-directory.

We then defined a function named display() that will actually render the sprite sheet. We first clear the rectangle where the sprite sheet will be displayed. The clearRect() function arguments must identify a rectangle. The first two argument specify the starting point (x, and y coordinates, respectively) of the rectangle. The next two arguments identify the width and height, respectively, of the rectangle. In our case, we’re saying that the entire canvas should be cleared before being re-painted.

Once the canvas is cleared, the image is rendered using the drawImage() function. The first argument is the Image object. The second and third arguments represent the coordinates on the canvas where the image will be rendered. We’ve requested that the image be rendered at the top left corner. We need to have the canvas API continuously refresh the image. We can either call the display() function recursively, or we can call the requestAnimationFrame() function with the function named (display) as the argument. Finally, we call the display() function.

With the above code, the page will render as follows:

Extracting an individual image from the sprite sheet

We don’t actually want to render the entire sprite sheet. I’m just looking for a frog’s top view — shown in the last row. We can pick any one of the sprites in that row. You need to pick the top left corner coordinates, the width and the height of a particular frog sprite from that sheet. You can use any tool that shows the current mouse coordinates when you hover over the part of an image. I used pixpsy, and figured out that my favourite frog image has the top left coordinate at (54, 152), and bottom right at (94, 182).

Now, we need to use an overloaded version of the drawImage() function, that allows us to show just a slice of the original image. Here’s the modified code:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img = new Image()
img.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

function display() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)
ctx2.drawImage(img, frogX, frogY, frogWidth, frogHeight, 0, 0, frogShowWidth, frogShowHeight)
requestAnimationFrame(display)
}

display()

I’ve set frogX and frogY to hold the coordinates of the top-left corner of the specific frog image in the sprite sheet. I calculated the frog width and height using arithmetic involving the top-left, and bottom-right corners of the frog image.

I decided that my frog image will be rendered on the screen with a height of 20 pixels, irrespective of its size in the sprite sheet. So, I’ll scale the image from the sprite sheet. I want to maintain the aspect ratio so that the image doesn’t get distorted. So, I calculated aspect ratio as the ratio of the height to the width of the frog in the sprite sheet.

Suppose that the frog in the image has a width of w1, and a height of h1. Also, suppose that you want to render the frog with a width of w2, and a height of 20 pixels. If you want the image to maintain the aspect ratio, then h1 / w1 = 20 / w2. Let the aspect ratio be a, then we can write a = 20 / w2. We’ve already calculated the aspect ratio, so we’ll determine w2 from the above equation as w2 = a / 20. That’s how we’re calculating frogShowWidth in the above code.

The call to drawImage() in the display() function has changed. Now, in addition to the first three arguments, which are the same as before, we’re passing some more. The third and fourth arguments are the width and height, respectively of the frog in the actual image. The fifth and sixth arguments are the location on the canvas where the frog image should be displayed. We chose 0, 0 to display it at the top left of the canvas element. The final two arguments are the width and the height, respectively, that we want the frog to appear on the canvas. Note the frog at the top left of the canvas in the following image:

The frog displayed at the top left of the canvas

Let’s move the frog to where it should initially be — the centre of the start lane. We’ll declare an object to hold the x and y coordinates of the frog and initialise that object to the x and y coordinates of the centre of the start lane. Here’s the modified code:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img = new Image()
img.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

function display() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)
ctx2.drawImage(img, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
requestAnimationFrame(display)
}

display()

Now, the frog will be displayed at the centre of the start lane:

Frog at its intended initial position

Show me the cars

Now, let’s render a car in the lane just above the frog. I’ll render it with a height of 40 pixels and width set to match the aspect ratio in the original image.

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

function animate2() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car1.x, car1.y, car1ShowWidth, car1ShowHeight)
requestAnimationFrame(animate2)
}

Now, we have two Image objects — one for the frog and the other for the cars. In order to extract the image of a particular car, I’ve defined car1X, car1Y, car1Width, and car1Height, just as I had done for the frog. I’ve also defined the car’s aspect ratio, set the display height of the car at 40 pixels, and calculated its display width according to the original aspect ratio.

I’ve defined a car1 object to hold the coordinates within the canvas element where this car will be shown. The particular car that I selected to display, here is heading towards the left, so I decided to display it at the right edge of the canvas element. Recall that the canvas element is 800 pixels wide. If I want to place a car with a width of car1ShowWidth at the very right edge, the x coordinate of its top-left corner must be CANWIDTH — car1ShowWidth. Similarly, the y coordinate of the top-left corner would be CANHEIGHT-20-car1ShowHeight. I’m taking away 20 pixels for the start lane and car1ShowHeight for the car’s height.

I’ve added a call to drawImage() — passing it the vehicles image with similar arguments as we did in the earlier call to drawImage(). With that, you should see something like the following:

Our first car on the canvas

A car in the next lane

In the second lane on the road (from the bottom), I’d like to show the same car heading in the opposite direction. Unfortunately, we don’t have an image of the same car heading in the opposite direction in the sprite sheet. We could copy paste it and rotate it in some image manipulation software. But there’s a way that we can show the same car rotated with the canvas API.

The canvas API provides a rotate() method that can be used to rotate the canvas around its origin by a specified angle in radians. So, let’s say we rotate our canvas by 180 degrees (or pi radians), then we have the situation as given below:

The rotate canvas is represented in the above figure by the one with the dashed outline. To make it coincide with the original canvas, we’ll apply the translate() method. We need to shift the rotated canvas by CANWIDTH pixels to the right, and CANHEIGHT pixels downward. That can be achieved with a call ctx2.translate(-CANWIDTH, -CANHEIGHT). So, let’s try out the following code:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

function animate3() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car1.x, car1.y, car1ShowWidth, car1ShowHeight)
ctx2.save()
ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, 0, 200, car1ShowWidth, car1ShowHeight)
ctx2.restore()
requestAnimationFrame(animate3)
}

animate3()

That should render something like the following:

Showing the rotated car

Note how the car is indeed rotated. Also note that specifying the x coordinate of 0 to render the rotated car now corresponds to the right edge of the canvas. As you increase the y coordinate from 200 to, say, 500, the car will move upwards. So, now our origin is at the bottom-right corner with x values increasing towards the left, and y values increasing upwards.

To get the car to appear at the left edge of the canvas in the second lane from the bottom, we’ll have to use an x coordinate of CANWIDHT — car1ShowWidth, and a y coordinate of 20 + car1ShowHeight. Let’s update the code with that:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

function animate3() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car1.x, car1.y, car1ShowWidth, car1ShowHeight)
ctx2.save()
ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car2.x, car2.y, car1ShowWidth, car1ShowHeight)
ctx2.restore()
requestAnimationFrame(animate3)
}

animate3()

Finally, note that we saved the canvas context before applying the rotation, and restored the context to its original state after rendering the rotated image. This is so that on the next call, the first car is rendered as usual on the original canvas orientation.

Animating the car

Now, let’s try to animate the first car so that it appears to be moving to the left. The key is to update the x and y coordinates within the canvas where the car is rendered. Here’s how we could do it:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const speed = 1

function animate3() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car1.x, car1.y, car1ShowWidth, car1ShowHeight)
car1.x = car1.x - speed
ctx2.save()
ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car2.x, car2.y, car1ShowWidth, car1ShowHeight)
ctx2.restore()
requestAnimationFrame(animate3)
}

animate3()

We’ve defined a constant named speed, and we’ve decremented car1’s x coordinate by that value inside animate3(). Since the canvas is cleared before rendering the image, the car will appear to move towards the left.

Making the car wrap around

Once the car reaches the left edge of the canvas, it disappears from view. To make it appear to wrap around from the right edge, we’ll reset car1.x to CANWIDTH. But, when do we do it? If we do it when car1.x becomes negative, then the car will disappear when its front bumper touches the left edge, and then appear from the right side. That’s not great. I’d like the car to gradually disappear from re-appearing from the right side.

When has the car completely disappeared from the view on the left hand side? When this happens, car1.x is at -car1ShowWidth. So, that’s what we’ll code, here:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const speed = 1

function animate3() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car1.x, car1.y, car1ShowWidth, car1ShowHeight)
car1.x = car1.x - speed
if (car1.x < -car1ShowWidth) {
car1.x = CANWIDTH
}
ctx2.save()
ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car2.x, car2.y, car1ShowWidth, car1ShowHeight)
ctx2.restore()
requestAnimationFrame(animate3)
}

animate3()

There we go! Now the first car loops around endlessly. We can do the same thing in the same way for the second car. But I’d rather put multiple cars in a row, first, and have them all loop endlessly.

By the way, if you want a different animation, your code will need to change accordingly. For example, if you want the car to start appearing from the right edge as soon as its front bumper disappears on the left, then you can use two drawImage() calls. The first call is as we’ve already done. The other will be for a “shadow” car — one that will be placed at 2*CANWIDTH, initially, and will follow the same sort of update code as the first car. I’m not going to code that here. On to multiple cars in a row!

Multiple cars in a row

Let’s put two cars in each lane. In the bottom lane, we’ll start with one car at the right edge of the canvas, and another car somewhere in the middle of the canvas. Similarly, the lane above it will have one car at the left edge of the canvas and another car somewhere in the middle.

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

let frogLocation = {
x: 400,
y: 520
}

const spacing1 = Math.floor((CANWIDTH - 2 * car1ShowWidth) / 2)

let row1 = []
for (let i = 0 ; i < 2 ; i++) {
row1[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 580 - car1ShowHeight}
}

let row2 = []
for (let i = 0 ; i < 2 ; i++) {
row2[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 20 + car1ShowHeight}
}

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const speed = 1

function animate3() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)
row1.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.save()
ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row2.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.restore()
row1 = row1.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
row2 = row2.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
requestAnimationFrame(animate3)
}

function incrementCheckAndReset(vehicle, speed, showWidth) {
return {
x: vehicle.x - speed < -showWidth ? CANWIDTH : vehicle.x - speed,
y: vehicle.y
}
}

animate3()

I’ve created arrays row1 and row2 for cars in the two rows. Just before the declaration of row1, I have calculated the spacing between the two cars in a row. The total width we have available is CANWIDTH. Out of that, 2*car1ShowWidth is occupied for the two cars, so that leaves us with CANWIDTH — 2*car1ShowWidth. One half of this will be used between the first and the second car, while the remaining will be used between the second car and the other end of the canvas.

The x coordinate of the first car in each row will be CANWIDTH — car1ShowWidth, while that of the second car will be CANWIDTH — (2*car1ShowWidth + spacing1). To make that calculation automatic, I’ve used a for loop. The y coordinate for both rows is calculated slightly differently, but the same way as we did previously when we had one car per row.

In order to animate the cars, I’ve used Array.map() with a named function incrementCheckAndReset() to update the values in the two arrays. This function updates the x coordinate value for a given car based on the value for speed that we set. It also ensures that a car that disappears from view on one end re-appears at the other end.

With the above code, you should have two cars in the two lanes driving around in a loop.

Multiple lanes with multiple cars

Let’s show two more lanes with another type of car, and a truck. The code is pretty much the same:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'


let frogLocation = {
x: 400,
y: 520
}

let scale2 = 1

let anim2 = 0
let frame = 0
let px = 0


const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const spacing1 = Math.floor((CANWIDTH - 2 * car1ShowWidth) / 2)

const speed1 = 1

const car2X = 440
const car2Y = 65
const car2Width = 529 - car2X
const car2Height = 107 - car2Y

const car2AspectRatio = car2Height / car2Width
const car2ShowWidth = 40 / car2AspectRatio
const car2ShowHeight = 40

const spacing2 = Math.floor((CANWIDTH - 2 * car2ShowWidth) / 2)

const speed2 = 1.5

const truckX = 8
const truckY = 230
const truckWidth = 97 - truckX
const truckHeight = 275 - truckY

const truckAspectRatio = truckHeight / truckWidth
const truckShowWidth = 40 / truckAspectRatio
const truckShowHeight = 40

const spacing3 = Math.floor((CANWIDTH - 2 * truckShowWidth) / 2)

const speed3 = 2

let row1 = []
for (let i = 0 ; i < 2 ; i++) {
row1[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: CANHEIGHT - 20 - car1ShowHeight}
}

let row2 = []
for (let i = 0 ; i < 2 ; i++) {
row2[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 20 + car1ShowHeight}
}

let row3 = []
for (let i = 0 ; i < 2 ; i++) {
row3[i] = {x: i * spacing2 + i * car2ShowWidth, y: CANHEIGHT - 20 - 2 * car1ShowHeight - car2ShowHeight}
}

let row4 = []
for (let i = 0 ; i < 2 ; i++) {
row4[i] = {x: i * spacing2 + (i + 1) * car2ShowWidth, y: 20 + car2ShowHeight + 2*car1ShowHeight}
}

let row5 = []
for (let i = 0 ; i < 2 ; i++) {
row5[i] = {x: i * spacing3 + i * truckShowWidth, y: CANHEIGHT - 20 - 2*car1ShowHeight - 2*car2ShowHeight - truckShowHeight}
}

let row6 = []
for (let i = 0 ; i < 2 ; i++) {
row6[i] = {x: i * spacing3 + i * truckShowWidth, y: 20 + truckShowHeight + 2*car1ShowHeight + 2*car2ShowHeight}
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car1Shadow = {
x: car1.x + CANWIDTH,
y: car1.y
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}


function animate2() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
row1.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))

ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row2.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.restore()

row1 = row1.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
row2 = row2.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))

row3.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row4.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.restore()

row3 = row3.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))
row4 = row4.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))

row5.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row6.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.restore()

row5 = row5.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
row6 = row6.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
requestAnimationFrame(animate2)

}

function incrementCheckAndReset(vehicle, speed, showWidth) {
return {
x: vehicle.x - speed < -showWidth ? CANWIDTH : vehicle.x - speed,
y: vehicle.y
}
}

animate2()

Showing the logs in the moat

The sprites that we have used earlier don’t have the image of a log. I downloaded a different one for it. Let’s show the log images in the moat and set them in motion. Here’s the code that we can start with:

const img3 = new Image()
img3.src = 'assets/logs.png'

const logX = 16
const logY = 258
const logWidth = 366 - 16
const logHeight = 314 - 258

const logAspectRatio = logHeight / logWidth
const logShowHeight = 40
const logShowWidth = logShowHeight / logAspectRatio

const logSpeed = 1

let logLocations = []

const logSpacing = Math.floor((CANWIDTH - 2*logShowWidth) / 2)

const stagger = 200

We need to load it into an Image object. Then, we need to figure out the coordinates within that image where the log image is located. We do this the same way we’ve done for the earlier sprite images. Accordingly, you see values of logX, logY, logWidth, and logHeight. We want the logs to have a height of 40 pixels, so that’s why logShowHeight is set to 40, and logShowWidth is set according to the height and the aspect ratio of the log within the image.

We define a single array to hold the locations of all the logs. We create it empty, initially, and then programmatically set the values in it. We define a variable logSpacing to indicate the spacing between two logs in the same row. We calculate this variable using the knowledge that the canvas width is shared between two logs in each row.

We also define a variable stagger to hold a value that will be used to offset the starting location of the first log in different rows. What do we mean by that? Let’s elaborate.

We want alternating rows of logs to be moving in opposite directions. Check. We want to initially place the first log in the first row on the extreme left. We want the first log in the second log initially on the right. If all pairs of rows of logs are placed this way, the following is what we see on the screen:

Placing logs without a stagger

This makes the game a bit easier to play. To make it a bit more challenging, we could add an offset to the logs in subsequent pairs of rows so that they appear as follows:

Staggering the logs in subsequent pairs of rows

This makes the game look a little less predictable. Now, onto setting the initial coordinates of the logs.

for (let i = 0 ; i < 3 ; i ++) {
logLocations.push({x: i*stagger, y: 20 + i * 80})
logLocations.push({x: logShowWidth + logSpacing + i*stagger, y: 20 + i * 80})
}

for (let i = 0 ; i < 3 ; i++) {
logLocations.push({x: CANWIDTH - logShowWidth - i * stagger, y: 60 + i * 80})
logLocations.push({x: CANWIDTH - 2*logShowWidth - logSpacing - i*stagger, y: 60 + i * 80})
}

In the first for loop, we place the logs that appear to move towards the left, while the second for loop is for the logs that appear to move towards the right. Each iteration of the first for loop places the two logs in a row. In other words, each call to push() is for one log in a specific row. Both loops run three times, for a total of six rows of logs, each with two logs.

The first statement in the first loop is for the log that appears on the left, while the other one is for the other log in that row. The y attribute for both of these logs is identical. The x attribute for the first row in each subsequent row of left-to-right moving logs increases by the value of the variable stagger. That’s why we see i*stagger . The first time through the loop, i.e., the first row of logs, the x attribute is 0. In the next iteration, for the third row of logs, the x attribute is equal to the value of stagger. Finally, for the fifth row, the x attribute is equal to 2*stagger.

The x attribute of the second log in each row is offset by the width of a log, and the spacing between two logs. That explains the x attribute in the second statement in the first for loop.

Finally, the y attribute is 20 + i * 80, because the first row is at an offset of 20 pixels from the top, while the third row starts 80 pixels below it. Why 80 pixels? Because in order to reach the top of the third row, from the top of the first row, you have to go down 40 pixels for the height of the first row, then another 40 pixels for the height of the second row of logs.

Since the logs moving towards the left are just a mirror reflection of the logs moving in the other direction, the second for loop differs from the first one mainly by the element of CANWIDTH. Recall in the 180 degree rotated realm, the x axis is 0 on the right edge of the canvas.

Finally, to animate the logs, we’ll place the following code in the animate() function:

logLocations.forEach(location => ctx2.drawImage(img3, logX, logY, logWidth, logHeight, location.x, location.y, logShowWidth, logShowHeight))

logLocations = logLocations.map((location, index) => {
let newLocation = location
if (index < 6) {
newLocation.x = newLocation.x + logSpeed > CANWIDTH ? -logShowWidth : newLocation.x + logSpeed
}
else {
newLocation.x = newLocation.x - logSpeed < -logShowWidth ? CANWIDTH : newLocation.x - logSpeed
}
return newLocation
})

The first line should be self-explanatory by now. It is the same as we’ve done for displaying the other sprites. Next comes the code to update the location of the logs so that they appear to move across the screen.

We use Array.map() on the logLocations array, with an anonymous function, for this purpose. We accept both the value of the element (location), as well as the index in this function. We first make a copy of the location, modify it, and finally return the modified value.

We update the location of the logs in alternating rows differently. Since we appended all the left-to-right logs into the logLocations array first, before appending the others, our update becomes somewhat convenient. The first six logs are updated the same way, i.e., by incrementing the x attribute. The remaining logs are updated by decrementing the x attribute. The first six logs have indices 0, 1, 2, 3, 4, and 5. If the index of the log we are processing is less than 6, we increment the x attribute by the logSpeed. We use the ternary operator to check if this increment causes the log to disappear from view on the right edge of the canvas. If so, we wrap the log around to the left of the canvas. The else part is also similar in nature.

Now, let’s see the code we have so far in its entirety:

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'
const img3 = new Image()
img3.src = 'assets/logs.png'

const logX = 16
const logY = 258
const logWidth = 366 - 16
const logHeight = 314 - 258

const logAspectRatio = logHeight / logWidth
const logShowHeight = 40
const logShowWidth = logShowHeight / logAspectRatio

const logSpeed = 1

let logLocations = []

const logSpacing = Math.floor((CANWIDTH - 2*logShowWidth) / 2)

const stagger = 200

let frogLocation = {
x: 400,
y: 520
}

let scale2 = 1

let anim2 = 0
let frame = 0
let px = 0


const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const spacing1 = Math.floor((CANWIDTH - 2 * car1ShowWidth) / 2)

const speed1 = 1

const car2X = 440
const car2Y = 65
const car2Width = 529 - car2X
const car2Height = 107 - car2Y

const car2AspectRatio = car2Height / car2Width
const car2ShowWidth = 40 / car2AspectRatio
const car2ShowHeight = 40

const spacing2 = Math.floor((CANWIDTH - 2 * car2ShowWidth) / 2)

const speed2 = 1.5

const truckX = 8
const truckY = 230
const truckWidth = 97 - truckX
const truckHeight = 275 - truckY

const truckAspectRatio = truckHeight / truckWidth
const truckShowWidth = 40 / truckAspectRatio
const truckShowHeight = 40

const spacing3 = Math.floor((CANWIDTH - 2 * truckShowWidth) / 2)

const speed3 = 2

let row1 = []
for (let i = 0 ; i < 2 ; i++) {
row1[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: CANHEIGHT - 20 - car1ShowHeight}
}

let row2 = []
for (let i = 0 ; i < 2 ; i++) {
row2[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 20 + car1ShowHeight}
}

let row3 = []
for (let i = 0 ; i < 2 ; i++) {
row3[i] = {x: i * spacing2 + i * car2ShowWidth, y: CANHEIGHT - 20 - 2 * car1ShowHeight - car2ShowHeight}
}

let row4 = []
for (let i = 0 ; i < 2 ; i++) {
row4[i] = {x: i * spacing2 + (i + 1) * car2ShowWidth, y: 20 + car2ShowHeight + 2*car1ShowHeight}
}

let row5 = []
for (let i = 0 ; i < 2 ; i++) {
row5[i] = {x: i * spacing3 + i * truckShowWidth, y: CANHEIGHT - 20 - 2*car1ShowHeight - 2*car2ShowHeight - truckShowHeight}
}

let row6 = []
for (let i = 0 ; i < 2 ; i++) {
row6[i] = {x: i * spacing3 + i * truckShowWidth, y: 20 + truckShowHeight + 2*car1ShowHeight + 2*car2ShowHeight}
}

for (let i = 0 ; i < 3 ; i ++) {
logLocations.push({x: i*stagger, y: 20 + i * 80})
logLocations.push({x: logShowWidth + logSpacing + i*stagger, y: 20 + i * 80})
}

for (let i = 0 ; i < 3 ; i++) {
logLocations.push({x: CANWIDTH - logShowWidth - i * stagger, y: 60 + i * 80})
logLocations.push({x: CANWIDTH - 2*logShowWidth - logSpacing - i*stagger, y: 60 + i * 80})
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car1Shadow = {
x: car1.x + CANWIDTH,
y: car1.y
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}


function animate2() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

logLocations.forEach(location => ctx2.drawImage(img3, logX, logY, logWidth, logHeight, location.x, location.y, logShowWidth, logShowHeight))

logLocations = logLocations.map((location, index) => {
let newLocation = location
if (index < 6) {
newLocation.x = newLocation.x + logSpeed > CANWIDTH ? -logShowWidth : newLocation.x + logSpeed
}
else {
newLocation.x = newLocation.x - logSpeed < -logShowWidth ? CANWIDTH : newLocation.x - logSpeed
}
return newLocation
})

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
row1.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))

ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row2.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.restore()

row1 = row1.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
row2 = row2.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))

row3.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row4.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.restore()

row3 = row3.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))
row4 = row4.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))

row5.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row6.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.restore()

row5 = row5.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
row6 = row6.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
requestAnimationFrame(animate2)

}

function incrementCheckAndReset(vehicle, speed, showWidth) {
return {
x: vehicle.x - speed < -showWidth ? CANWIDTH : vehicle.x - speed,
y: vehicle.y
}
}

animate2()

With the above code, the logs should also appear to be animated.

Set the frog in motion

Now, we add the frog’s movement on keystrokes.

document.addEventListener('keydown', handleKey)
function handleKey(e) {
switch(e.key) {
case 'ArrowRight':
if (frogLocation.x < CANWIDTH - frogShowWidth)
frogLocation.x+=20
break

case 'ArrowLeft':
if (frogLocation.x > 0)
frogLocation.x-=20
break
case 'ArrowUp':
if (frogLocation.y > 0)
frogLocation.y -= 20
break

case 'ArrowDown':
if(frogLocation.y < CANHEIGHT - frogShowHeight)
frogLocation.y += 20
break
}
}

We add an event listener on the key down event. In the callback function for this event, we check the key that was pressed. We respond to up, down, left, and right keys only. In response to the up arrow key, we decrement the y attribute of frogLocation to make the frog appear to move upwards. The rendering of the frog at the new location will be handled by the drawImage() call in the animate() function. Similarly, the other keys are handled. In all of the processing, we are mindful of the frog not going beyond the canvas dimensions.

Once we add the above code to what we had so far, we’ll be able to move the frog around the screen. An important thing to notice is that since we had the drawImage() for the logs before that for the frog, the frog will appear to be above the logs. On the other hand, since all the vehicles were drawn after the frog, the frog will appear to pass under the vehicles. That is the behaviour we’d want to see.

Getting the frog to ride on the logs

Next, let’s make sure that when a frog hops onto a log, it appears to ride it. We’ll need to detect when a frog is on a log, and then update the frog’s location when we update the log location. Here’s the updated code:

logLocations = logLocations.map((location, index) => {

let newLocation = location
if (index < 6) {
if(isFrogOnLog(location)) {
frogLocation.x += logSpeed
}
newLocation.x = newLocation.x + logSpeed > CANWIDTH ? -logShowWidth : newLocation.x + logSpeed
}
else {
if(isFrogOnLog(location)) {
frogLocation.x -= logSpeed
}
newLocation.x = newLocation.x - logSpeed < -logShowWidth ? CANWIDTH : newLocation.x - logSpeed
}
return newLocation
})

function isFrogOnLog(logLocation) {
if (
(frogLocation.y + frogShowHeight <= logLocation.y + logShowHeight) &&
(frogLocation.y >= logLocation.y) &&
(frogLocation.x >= logLocation.x) &&
(frogLocation.x <= logLocation.x + logShowWidth - frogShowWidth)
){
return true
}

else {
return false
}
}

The isFrogOnLog() function is called for each log. In this function, the first two conditions check for the frog being in the row of a given log. The first condition checks that the frog is above or on the lower edge of the log row. The second condition checks that the frog is below or on the upper edge of the log row. If both of these conditions are true, then the frog is indeed in that log’s row. The next thing we check is that the frog is within the horizontal boundary of the log in that row. The third condition checks that the frog is to on or to the right of the left edge of the log. The fourth one checks that the frog is on or to the left of the right edge of the log. If all four of these conditions evaluate to true, then we conclude that the frog is indeed on this log, and return true. Otherwise, we return false.

We’ve introduced the calls to this function within the log position update code, so that whenever we update the position of a log, we check if the frog is also on that log. If so, we update the frogLocation variable, as well. The direction of update (increment, or decrement) depends on the direction of the log. At this point, you should be able to have the frog ride the logs.

Stopping the game on drowning

If the frog jumps in the water, the game should stop. Let’s implement that.

function isFrogDrowned() {
if (frogLocation.y > 20 && frogLocation.y <= 240) {
if (!logLocations.some(location => isFrogOnLog(location))) {
return true
}
}
else
return false
}

We implement a function named isFrogDrowned() that checks if the frog is in the part of the canvas that represents the moat. Since the moat starts after a 20 pixel green belt, the moat’s y coordinate starts after 20 pixels. That’s why you see a comparison of frog’s y coordinate with 20 in the if statement. Since there are six rows of logs, each occupying 40 pixels height, the moat y coordinate goes till 260 pixels. But the condition in the if statement compares the frog’s y coordinate against 240. Why is that? This is because the frog itself has a height of 20 pixels, and the location that we have represents the top left corner of the frog. When this location has a y coordinate of 240 pixels, the bottom of the frog is at 260 pixels. Under this condition, the frog would be fully immersed in the moat’s bottom edge.

If the above conditions both evaluate to true, we check if the frog is on a log. For this, we call Array.some() on the logLocations array. If the isFrogOnLog() function returns true for any of the elements of the logLocations array, the Array.some() function returns true. Otherwise, it returns false. To check if the frog is NOT on a log, we invert the value returned by Array.some() using the ! operator. If the frog is in the moat and not on a log, we return true.

If the isFrogDrowned() function returns true, we should stop the game animation and the key press handling. Well, for all practical purposes, if we stop the animation, we’re fine because after that, the player wouldn’t be able to see the effect of the key press handler. Here’s how we may update the animate() function.

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'
const img3 = new Image()
img3.src = 'assets/logs.png'

const logX = 16
const logY = 258
const logWidth = 366 - 16
const logHeight = 314 - 258

const logAspectRatio = logHeight / logWidth
const logShowHeight = 40
const logShowWidth = logShowHeight / logAspectRatio

const logSpeed = 1

let logLocations = []

const logSpacing = Math.floor((CANWIDTH - 2*logShowWidth) / 2)

const stagger = 200

let frogLocation = {
x: 400,
y: 520
}

let scale2 = 1

let anim2 = 0
let frame = 0
let px = 0

const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const spacing1 = Math.floor((CANWIDTH - 2 * car1ShowWidth) / 2)

const speed1 = 1

const car2X = 440
const car2Y = 65
const car2Width = 529 - car2X
const car2Height = 107 - car2Y

const car2AspectRatio = car2Height / car2Width
const car2ShowWidth = 40 / car2AspectRatio
const car2ShowHeight = 40

const spacing2 = Math.floor((CANWIDTH - 2 * car2ShowWidth) / 2)

const speed2 = 1.5

const truckX = 8
const truckY = 230
const truckWidth = 97 - truckX
const truckHeight = 275 - truckY

const truckAspectRatio = truckHeight / truckWidth
const truckShowWidth = 40 / truckAspectRatio
const truckShowHeight = 40

const spacing3 = Math.floor((CANWIDTH - 2 * truckShowWidth) / 2)

const speed3 = 2

let row1 = []
for (let i = 0 ; i < 2 ; i++) {
row1[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: CANHEIGHT - 20 - car1ShowHeight}
}

let row2 = []
for (let i = 0 ; i < 2 ; i++) {
row2[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 20 + car1ShowHeight}
}

let row3 = []
for (let i = 0 ; i < 2 ; i++) {
row3[i] = {x: i * spacing2 + i * car2ShowWidth, y: CANHEIGHT - 20 - 2 * car1ShowHeight - car2ShowHeight}
}

let row4 = []
for (let i = 0 ; i < 2 ; i++) {
row4[i] = {x: i * spacing2 + (i + 1) * car2ShowWidth, y: 20 + car2ShowHeight + 2*car1ShowHeight}
}

let row5 = []
for (let i = 0 ; i < 2 ; i++) {
row5[i] = {x: i * spacing3 + i * truckShowWidth, y: CANHEIGHT - 20 - 2*car1ShowHeight - 2*car2ShowHeight - truckShowHeight}
}

let row6 = []
for (let i = 0 ; i < 2 ; i++) {
row6[i] = {x: i * spacing3 + i * truckShowWidth, y: 20 + truckShowHeight + 2*car1ShowHeight + 2*car2ShowHeight}
}

for (let i = 0 ; i < 3 ; i ++) {
logLocations.push({x: i*stagger, y: 20 + i * 80})
logLocations.push({x: logShowWidth + logSpacing + i*stagger, y: 20 + i * 80})
}

for (let i = 0 ; i < 3 ; i++) {
logLocations.push({x: CANWIDTH - logShowWidth - i * stagger, y: 60 + i * 80})
logLocations.push({x: CANWIDTH - 2*logShowWidth - logSpacing - i*stagger, y: 60 + i * 80})
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car1Shadow = {
x: car1.x + CANWIDTH,
y: car1.y
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}

function animate2() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

logLocations.forEach(location => ctx2.drawImage(img3, logX, logY, logWidth, logHeight, location.x, location.y, logShowWidth, logShowHeight))

logLocations = logLocations.map((location, index) => {
let newLocation = location
if (index < 6) {
newLocation.x = newLocation.x + logSpeed > CANWIDTH ? -logShowWidth : newLocation.x + logSpeed
}
else {
newLocation.x = newLocation.x - logSpeed < -logShowWidth ? CANWIDTH : newLocation.x - logSpeed
}
return newLocation
})

ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)
row1.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))

ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row2.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.restore()

row1 = row1.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
row2 = row2.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))

row3.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row4.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.restore()

row3 = row3.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))
row4 = row4.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))

row5.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row6.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.restore()

row5 = row5.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
row6 = row6.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))

if (isFrogDrowned())
return
requestAnimationFrame(animate2)

}

function incrementCheckAndReset(vehicle, speed, showWidth) {
return {
x: vehicle.x - speed < -showWidth ? CANWIDTH : vehicle.x - speed,
y: vehicle.y
}
}

animate2()

Since we return from the function before calling requestAnimationFrame(), the animation stops.

Implementing winning

If the frog reaches the top green row, it wins. We can implement it using the following function:

function isFrogWon() {
if (frogLocation.y < 20) {
return true
}
else
return false
}

This function checks if the frog’s y coordinate has reached a value less than 20. If so, it returns true, or false otherwise. The frog’s y coordinate will be at least 20 on the first row of logs, so this should work. We’ll add a call to this function towards the end of the animate2() function as:

function animate2() {
// Rest of the code

if (isFrogDrowned())
return
if (isFrogWon())
return
requestAnimationFrame(animate2)

}

Implementing run over by a vehicle

To check for the frog having been hit by a vehicle, we’ll implement a function named isFrogHit(), which accepts the vehicle coordinates, width, and height as arguments and returns true if the frog has hit that vehicle, or false otherwise.

function isFrogHit(vehicle, vehicleWidth, vehicleHeight) {
if (frogLocation.y >= vehicle.y &&
frogLocation.y + frogShowHeight <= vehicle.y + vehicleHeight &&
frogLocation.x + frogShowWidth >= vehicle.x &&
frogLocation.x <= vehicle.x + vehicleWidth
)
return true

return false
}

The if statement is similar to what we had in the isFrogOnLog() function, so no explanations should be necessary, here. We’ll add calls to this function for all the rows of vehicles near the end of the animate2() function, as follows:

function animate2() {
// Rest of the code

if (isFrogWon())
return
if (row1.some((car) => isFrogHit(car, car1ShowWidth, car1ShowHeight)))
return
if (row2.some((car) => isFrogHit(car, car1ShowWidth, car1ShowHeight)))
return
if (row3.some((car) => isFrogHit(car, car2ShowWidth, car2ShowHeight)))
return
if (row4.some((car) => isFrogHit(car, car2ShowWidth, car2ShowHeight)))
return
if (row5.some((truck) => isFrogHit(truck, truckShowWidth, truckShowHeight)))
return
if (row6.some((truck) => isFrogHit(truck, truckShowWidth, truckShowHeight)))
return
requestAnimationFrame(animate2)
}

However, the above code doesn’t work for the vehicles going form right to left. Why is that? The problem lies in the rotation that we applied. The coordinate system has changed. The x coordinate increases from left to right, the y coordinate from bottom to top, and the origin is at the bottom right corner.

However, a simple transformation should convert the coordinates to be consistent with the isFrogHit() function. We’ll map the rotated and translated coordinates of the reversed vehicles to the corresponding coordinates of the same point on the original canvas. The following function does the trick:

function convertCoord(vehicle, vehicleWidth, vehicleHeight) {
return {
x: CANWIDTH - vehicle.x - vehicleWidth,
y: CANHEIGHT - vehicle.y - vehicleHeight
}
}

We are calculating the coordinate of the top-left corner of the vehicle in the original coordinate system given the coordinates of the bottom right of the vehicle in the rotated and translated coordinate system.

If the bottom right x coordinate in the rotated and translated coordinate system is vehicle.x, then the x coordinate of the same point in the original coordinate system is CANWIDTH — vehicle.x. The x coordinate of the top-left corner, then, is CANWIDTH-vehicle.x-vehicleWidth. The y coordinate of the top-left corner in the original coordinate system is calculated similarly. Now, let’s insert calls to this function in ainmate2():

function animate2() {
// Rest of the code

if (isFrogWon())
return
if (row1.some((car) => isFrogHit(car, car1ShowWidth, car1ShowHeight)))
return
if (row2.some((car) => isFrogHit(convertCoord(car, car1ShowWidth, car1ShowHeight), car1ShowWidth, car1ShowHeight)))
return
if (row3.some((car) => isFrogHit(car, car2ShowWidth, car2ShowHeight)))
return
if (row4.some((car) => isFrogHit(convertCoord(car, car2ShowWidth, car2ShowHeight), car2ShowWidth, car2ShowHeight)))
return
if (row5.some((truck) => isFrogHit(truck, truckShowWidth, truckShowHeight)))
return
if (row6.some((truck) => isFrogHit(convertCoord(truck, truckShowWidth, truckShowHeight), truckShowWidth, truckShowHeight)))
return
requestAnimationFrame(animate2)
}

With this, collisions with any of the vehicles should result in the animation stopping.

Time over

The game should be timed. Once the timer expires, if the frog hasn’t reached the destination, it is game over. We need to show a timer to the player so that they know how much time is left. Here’s the updated HTML:

<!DOCTYPE html>
<html>
<head>
<title>Frogger Game</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="progress-bar">
<div class="progress">
</div>
</div>
<div class="game-container">
<div class="finish-line"></div>
<div class="blue-background"></div>
<div class="green-background"></div>
<div class="dark-gray-background"></div>
<div class="start-line"></div>
<canvas id="frogger-canvas" width="800" height="600"></canvas>
</div>
<script src="app.js"></script>
</body>
</html>

The styles for the new HTML elements are:

.game-container {
width: 800px;
height: 540px;
margin: 0 auto;
margin-top: 10px;
position: relative;
top: 10px;
}
.progress-bar {
width: 300px;
height: 20px;
background-color: #f2f2f2;
position: relative;
top: 10px;
margin: 0 auto;
}

.progress {
height: 100%;
background-color: #4caf50;
position: absolute;
top: 0;
left: 0;
width: 0%;
}

We create a div element with a class of progress-bar. This div element has a light background colour. We create the sense of passage of time through another nested div element that has a darker green background colour. This div occupies 0% of the parent div element’s width, and all of the parent element’s height. We also update the game-container class with a top property of 10 px to make room for the progress bar.

You should be able to see an empty progress bar near the top of the page. Now, for the code to make this progress bar fill up, gradually.

const pBar = document.querySelector('.progress')

let progress = 0
const TIMELIMIT = 2000
let pTimer = null

function updateTimer() {
progress += 1
const percentage = ((progress) / TIMELIMIT) * 100
pBar.style.width = percentage + '%'
if(progress < TIMELIMIT)
pTimer = setTimeout(updateTimer, 20)
}

updateTimer()

We acquire an object corresponding to the progress bar. We initialise the progress to 0. We set a constant to represent the maximum game duration. We create a variable to hold the timer that we create later.

In the updateTimer() function, we increment progress. We calculate the progress percentage and update the width property of the progress bar. If a timeout doesn’t occur, we call setTimeout() to re-invoke this function after 20 ms. We finally insert a call to this function.

With the above additions to the code, you should be able to see an animated progress bar that gradually fills up to 100%. But the game doesn’t stop once the timer expires. To do that, we’ll update our animate2() function.

function animate2() {
// Rest of the code

if (progress >= TIMELIMIT)
return
requestAnimationFrame(animate2)
}

We just need to compare the value of progress against that of TIMELIMIT. If we find that the time limit has been reached, we don’t animate the game any more.

You might notice that at this point, if you win the game, the progress bar keeps animating. To fix that, we could add a call to isFrogWon() in the if statement in the updateTimer() function as follows:

function updateTimer() {
progress += 1
const percentage = ((progress) / TIMELIMIT) * 100
pBar.style.width = percentage + '%'
if(progress < TIMELIMIT && !isFrogWon())
pTimer = setTimeout(updateTimer, 20)
}

That’s all folks

We stop at this point. Here’s the complete HTML for this game:

<!DOCTYPE html>
<html>
<head>
<title>Frogger Game</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="progress-bar">
<div class="progress">
</div>
</div>
<div class="game-container">
<div class="finish-line"></div>
<div class="blue-background"></div>
<div class="green-background"></div>
<div class="dark-gray-background"></div>
<div class="start-line"></div>
<canvas id="frogger-canvas" width="800" height="600"></canvas>
</div>
<script src="app.js"></script>
</body>
</html>

Here’s the CSS:

body {
margin: 0;
padding: 0;
}

.game-container {
width: 800px;
height: 540px;
margin: 0 auto;
margin-top: 10px;
position: relative;
top: 10px;
}

.blue-background {
width: 800px;
height: 240px;
background-color: blue;
}

.green-background {
width: 800px;
height: 20px;
background-color: green;
}

.dark-gray-background {
width: 800px;
height: 240px;
background-color: darkgray;
}

.finish-line, .start-line {
width: 800px;
height: 20px;
background-color: green;
}

#frogger-canvas {
position: absolute;
top: 0;
left: 0;
z-index: 1; /* Place the canvas above the background */
}

.progress-bar {
width: 300px;
height: 20px;
background-color: #f2f2f2;
position: relative;
top: 10px;
margin: 0 auto;
}

.progress {
height: 100%;
background-color: #4caf50;
position: absolute;
top: 0;
left: 0;
width: 0%;
}

Here’s the JavaScript:

const pBar = document.querySelector('.progress')

let progress = 0
const TIMELIMIT = 2000
let pTimer = null

const cvs2 = document.getElementById('frogger-canvas')
const ctx2 = cvs2.getContext('2d')

const CANWIDTH = cvs2.width = 800
const CANHEIGHT = cvs2.height = 540

const img1 = new Image()
img1.src = 'assets/26755.png'
const img2 = new Image()
img2.src = 'assets/frogger.png'
const img3 = new Image()
img3.src = 'assets/logs.png'

let frogLocation = {
x: 400,
y: 520
}

let scale2 = 1

let anim2 = 0
let frame = 0
let px = 0


const frogX = 54
const frogY = 152
const frogWidth = 94 - frogX
const frogHeight = 182 - frogY

const frogAspectRatio = frogHeight / frogWidth

const frogShowWidth = 20 / frogAspectRatio
const frogShowHeight = 20

const car1X = 408
const car1Y = 175
const car1Width = 490 - car1X
const car1Height = 220 - car1Y

const car1AspectRatio = car1Height / car1Width
const car1ShowWidth = 40 / car1AspectRatio
const car1ShowHeight = 40

const spacing1 = Math.floor((CANWIDTH - 2 * car1ShowWidth) / 2)

const speed1 = 1

const car2X = 440
const car2Y = 65
const car2Width = 529 - car2X
const car2Height = 107 - car2Y

const car2AspectRatio = car2Height / car2Width
const car2ShowWidth = 40 / car2AspectRatio
const car2ShowHeight = 40

const spacing2 = Math.floor((CANWIDTH - 2 * car2ShowWidth) / 2)

const speed2 = 1.5

const truckX = 8
const truckY = 230
const truckWidth = 97 - truckX
const truckHeight = 275 - truckY

const truckAspectRatio = truckHeight / truckWidth
const truckShowWidth = 40 / truckAspectRatio
const truckShowHeight = 40

const spacing3 = Math.floor((CANWIDTH - 2 * truckShowWidth) / 2)

const speed3 = 2

let row1 = []
for (let i = 0 ; i < 2 ; i++) {
row1[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: CANHEIGHT - 20 - car1ShowHeight}
}

let row2 = []
for (let i = 0 ; i < 2 ; i++) {
row2[i] = {x: CANWIDTH - (i * spacing1 + (i + 1) * car1ShowWidth), y: 20 + car1ShowHeight}
}

let row3 = []
for (let i = 0 ; i < 2 ; i++) {
row3[i] = {x: i * spacing2 + i * car2ShowWidth, y: CANHEIGHT - 20 - 2 * car1ShowHeight - car2ShowHeight}
}

let row4 = []
for (let i = 0 ; i < 2 ; i++) {
row4[i] = {x: i * spacing2 + (i + 1) * car2ShowWidth, y: 20 + car2ShowHeight + 2*car1ShowHeight}
}

let row5 = []
for (let i = 0 ; i < 2 ; i++) {
row5[i] = {x: i * spacing3 + i * truckShowWidth, y: CANHEIGHT - 20 - 2*car1ShowHeight - 2*car2ShowHeight - truckShowHeight}
}

let row6 = []
for (let i = 0 ; i < 2 ; i++) {
row6[i] = {x: i * spacing3 + i * truckShowWidth, y: 20 + truckShowHeight + 2*car1ShowHeight + 2*car2ShowHeight}
}

let car1 = {
x: CANWIDTH - car1ShowWidth,
y: CANHEIGHT - 20 - car1ShowHeight,
speed: 1
}

let car1Shadow = {
x: car1.x + CANWIDTH,
y: car1.y
}

let car2 = {
x: CANWIDTH - car1ShowWidth,
y: car1ShowHeight + 20
}

const logX = 16
const logY = 258
const logWidth = 366 - 16
const logHeight = 314 - 258

const logAspectRatio = logHeight / logWidth
const logShowHeight = 40
const logShowWidth = logShowHeight / logAspectRatio

const logSpeed = 1

let logLocations = []

const logSpacing = Math.floor((CANWIDTH - 2*logShowWidth) / 2)

const stagger = 200

for (let i = 0 ; i < 3 ; i ++) {
logLocations.push({x: i*stagger, y: 20 + i * 80})
logLocations.push({x: logShowWidth + logSpacing + i*stagger, y: 20 + i * 80})
}

for (let i = 0 ; i < 3 ; i++) {
logLocations.push({x: CANWIDTH - logShowWidth - i * stagger, y: 60 + i * 80})
logLocations.push({x: CANWIDTH - 2*logShowWidth - logSpacing - i*stagger, y: 60 + i * 80})
}

document.addEventListener('keydown', handleKey)
function handleKey(e) {
switch(e.key) {
case 'ArrowRight':
if (frogLocation.x < CANWIDTH - frogShowWidth)
frogLocation.x+=20
break

case 'ArrowLeft':
if (frogLocation.x > 0)
frogLocation.x-=20
break
case 'ArrowUp':
if (frogLocation.y > 0)
frogLocation.y -= 20
break

case 'ArrowDown':
if(frogLocation.y < CANHEIGHT - frogShowHeight)
frogLocation.y += 20
break
}
}

function animate2() {
ctx2.clearRect(0, 0, CANWIDTH, CANHEIGHT)

logLocations.forEach(location => ctx2.drawImage(img3, logX, logY, logWidth, logHeight, location.x, location.y, logShowWidth, logShowHeight))

logLocations = logLocations.map((location, index) => {

let newLocation = location
if (index < 6) {
if(isFrogOnLog(location)) {
frogLocation.x += logSpeed
}
newLocation.x = newLocation.x + logSpeed > CANWIDTH ? -logShowWidth : newLocation.x + logSpeed
}
else {
if(isFrogOnLog(location)) {
frogLocation.x -= logSpeed
}
newLocation.x = newLocation.x - logSpeed < -logShowWidth ? CANWIDTH : newLocation.x - logSpeed
}
return newLocation
})
ctx2.drawImage(img2, frogX, frogY, frogWidth, frogHeight, frogLocation.x, frogLocation.y, frogShowWidth, frogShowHeight)

row1.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))

ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row2.forEach( car => ctx2.drawImage(img1, car1X, car1Y, car1Width, car1Height, car.x, car.y, car1ShowWidth, car1ShowHeight))
ctx2.restore()

row1 = row1.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))
row2 = row2.map((car) => incrementCheckAndReset(car, speed1, car1ShowWidth))

row3.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row4.forEach( car => ctx2.drawImage(img1, car2X, car2Y, car2Width, car2Height, car.x, car.y, car2ShowWidth, car2ShowHeight))
ctx2.restore()

row3 = row3.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))
row4 = row4.map((car) => incrementCheckAndReset(car, speed2, car2ShowWidth))

row5.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.save()

ctx2.rotate(Math.PI)
ctx2.translate(-CANWIDTH, -CANHEIGHT)
row6.forEach( truck => ctx2.drawImage(img1, truckX, truckY, truckWidth, truckHeight, truck.x, truck.y, truckShowWidth, truckShowHeight))
ctx2.restore()

row5 = row5.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))
row6 = row6.map((truck) => incrementCheckAndReset(truck, speed3, truckShowWidth))

if (isFrogDrowned())
return
if (isFrogWon())
return
if (row1.some((car) => isFrogHit(car, car1ShowWidth, car1ShowHeight)))
return
if (row2.some((car) => isFrogHit(convertCoord(car, car1ShowWidth, car1ShowHeight), car1ShowWidth, car1ShowHeight)))
return
if (row3.some((car) => isFrogHit(car, car2ShowWidth, car2ShowHeight)))
return
if (row4.some((car) => isFrogHit(convertCoord(car, car2ShowWidth, car2ShowHeight), car2ShowWidth, car2ShowHeight)))
return
if (row5.some((truck) => isFrogHit(truck, truckShowWidth, truckShowHeight)))
return
if (row6.some((truck) => isFrogHit(convertCoord(truck, truckShowWidth, truckShowHeight), truckShowWidth, truckShowHeight)))
return
if (progress >= TIMELIMIT)
return
requestAnimationFrame(animate2)

}

function incrementCheckAndReset(vehicle, speed, showWidth) {
return {
x: vehicle.x - speed < -showWidth ? CANWIDTH : vehicle.x - speed,
y: vehicle.y
}
}

function isFrogOnLog(logLocation) {
if (
(frogLocation.y + frogShowHeight <= logLocation.y + logShowHeight) &&
(frogLocation.y >= logLocation.y) &&
(frogLocation.x >= logLocation.x) &&
(frogLocation.x <= logLocation.x + logShowWidth - frogShowWidth)
){
return true
}

else {
return false
}
}

function isFrogDrowned() {
if (frogLocation.y > 20 && frogLocation.y <= 240) {
if (!logLocations.some(location => isFrogOnLog(location))) {
return true
}
}
else
return false
}

function isFrogWon() {
if (frogLocation.y < 20) {
return true
}
else
return false
}

function isFrogHit(vehicle, vehicleWidth, vehicleHeight) {
if (frogLocation.y >= vehicle.y &&
frogLocation.y + frogShowHeight <= vehicle.y + vehicleHeight &&
frogLocation.x + frogShowWidth >= vehicle.x &&
frogLocation.x <= vehicle.x + vehicleWidth
)
return true

return false
}

function convertCoord(vehicle, vehicleWidth, vehicleHeight) {
return {
x: CANWIDTH - vehicle.x - vehicleWidth,
y: CANHEIGHT - vehicle.y - vehicleHeight
}
}

function updateTimer() {
progress += 1
const percentage = ((progress) / TIMELIMIT) * 100
pBar.style.width = percentage + '%'
if(progress < TIMELIMIT && !isFrogWon())
pTimer = setTimeout(updateTimer, 20)
}

updateTimer()

animate2()

You may download the entire source code from here. You may extend and improve this game in various ways:

  • Display a message when the game is won or lost
  • Create logs that disappear (like in the original game)
  • Have turtles float in the moat which eat the frog
  • Have a “rogue” frog on some of the logs
  • Create various levels in the game

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation