back to writing / back to bedroom

If you made it through Part 2 of this tutorial, you should now have a room with a character in it that can walk around! Now, let’s add a piece of furniture for it to interact with.

I’m adding in this desk.

If you followed Part 1, it will already be in your HTML file, so now all we need to do is use Javascript to draw the image in the canvas.

Add a line like this into the variables section of your Javascript file:


const deskImg = document.getElementById("deskImg");

Go ahead and add the following variables to determine the size and position of the furniture too (you can fiddle with the numbers later, once you can see the image on your canvas):


const deskWidth = 108;
const deskHeight = 80;
const deskDepth = 20;
const deskX = (canvas.width - deskWidth) / 2;
const deskY = (wallHeight - deskDepth);

Lastly, we need to add it into our draw() function. Keep in mind that things are drawn onto the canvas in the order that they are written — furniture should be placed after floors and walls so that it stays on top of them. Whether you want it to be above or below your character sprite is up to you (click here for an example of how I made the desk chair in my bedroom stay below the sprite if the sprite is in front of it, and above the sprite if the sprite is behind it).

Since the back of the chair is at about the same height as the front of my desk, or deskY2, I simply changed the order that the chair and character sprite were drawn in depending on whether the character sprite was below or above deskY2.


if (spriteY + spriteHeight < deskY2) {
    if (sprite.complete && sprite.naturalWidth !== 0) {
        ctx.drawImage(sprite, spriteX, spriteY, spriteWidth, spriteHeight);
    }
    if (chairImg.complete && chairImg.naturalWidth !== 0) {
        ctx.drawImage(chairImg, deskX + 90, deskY + deskHeight - 35, 38, 64);
    }
} else {
    if (chairImg.complete && chairImg.naturalWidth !== 0) {
        ctx.drawImage(chairImg, deskX + 90, deskY + deskHeight - 35, 38, 64);
    }
    if (sprite.complete && sprite.naturalWidth !== 0) {
        ctx.drawImage(sprite, spriteX, spriteY, spriteWidth, spriteHeight);
    }
}

I’m placing my table at the back of the room from our perspective, so it should be drawn before the sprite.


if (deskImg.complete && deskImg.naturalWidth !== 0) {
    ctx.drawImage(deskImg, deskX, deskY, deskWidth, deskHeight);
}

If your furniture is looking a bit blurry, add the highlighted line into your CSS file:


* {
    padding: 0;
    margin: 0;
    image-rendering: pixelated; 
}

This disables anti-aliasing. Your pixel assets should all look a lot clearer now.

Once you’re happy with the size and position of your furniture, we need to make sure the sprite can’t walk all over it. For this, we need to add more parametres into our willCollide() function that define the space taken up by our furniture. First, let’s add the following into our variables list do help us define that space:


const deskX1 = ((canvas.width - deskWidth) / 2)
const deskX2 = ((canvas.width - deskWidth) / 2) + deskWidth
const deskY2 = wallHeight + deskHeight - deskDepth;

deskX1 and deskX2 mark the two edges of the desk on the horizontal axis, while deskY2 is its second point on the vertical axis (since my desk is up against the wall, deskY1 would simply be the same as our wallHeight, which we’ve already defined as a space the sprite can’t walk into).

Now, I’m adding the space into my willCollide() function:


function willCollide(newX, newY) {
    return (
        newY + spriteHeight < wallHeight + dividerHeight + 5 ||
        newX + spriteWidth > deskX1 &&
        newX < deskX2 &&
        newY + spriteHeight < deskY2
    )
}

The canvas counts your sprite’s coordinates from its top-left corner: this means you need to add the spriteWidth to newX to make sure the sprite itself stays out of the boundaries of the desk on the left.

Now you can add infinite collidable furniture to your room with just a little math. I’m going to show you one last thing: how to make your furniture interactable.

This requires more math. I’ve created a function for determining whent the sprite is next to the desk:


function isAdjacentToDesk() {
    const margin = 15;
    const nearLeft = Math.abs((spriteX + spriteWidth) - deskX) <= margin && spriteY + spriteHeight > deskY && spriteY < deskY + deskHeight;
    const nearRight = Math.abs(spriteX - (deskX + deskWidth)) <= margin && spriteY + spriteHeight > deskY && spriteY < deskY + deskHeight;
    const nearBottom = Math.abs(spriteY - (deskY + deskHeight - spriteHeight)) <= margin && spriteX + spriteWidth > deskX && spriteX < deskX + deskWidth;
    return nearLeft || nearRight || nearBottom;
}

Margin gives us some extra room — the sprite doesn’t have to be exactly next to the desk for it to become selectable.

nearLeft is triggered if a) the sprite’s position on the horizontal axis is closer to the desk’s position on the left than the margin and b) the sprite’s position on the vertical axis is between deskY and deskY + deskHeight (aka the area the desk takes up on the Y axis). If both those things are true, the sprite is near the left boundary of the desk.

I hope that made sense. nearRight and nearBottom use the same logic. If your furniture item also has a top boundary that isn’t against the wall, you’ll need to add a nearTop as well.

Lastly, we need to call the function inside our draw() function. Right above where I’ve drawn my desk, I’m adding this line:


deskSelectable = isAdjacentToDesk();

And now, I’m going to change the part of the draw() function that draws the desk to also add a simple white border around the desk if deskSelectable is true:


if (deskImg.complete && deskImg.naturalWidth !== 0) {
    ctx.drawImage(deskImg, deskX, deskY, deskWidth, deskHeight);
    if (deskSelectable) {
        ctx.save();
        ctx.strokeStyle = "white";
        ctx.lineWidth = 4;
        ctx.strokeRect(deskX - 2, deskY - 2, deskWidth + 4, deskHeight + 4);
        ctx.restore();
    }
}

Wow! It works.

Lastly, I want something to happen when the desk is actually selected. I’m going to start by adding a simple div to my HTML file, under my <div class = “hidden”></div>:


<div id="desk" class = "element">
    <h2>writing desk</h2>
</div>

And then in my CSS file I’m going to make sure it’s hidden:


.element {
    display: none;
}

Back in our Javascript file, I’m going to add this right before our startGame() function:


let deskOpen = false;

And now, inside our startGame() function, I’m going to add event listeners for the space bar that will check if the desk is selectable or not. If it is, the new div I made will become visible.

I’m adding this to the end of our function keyDownHandler(e), after the four direction keys:


else if (e.key === " " || e.key === "Spacebar") {
    if (deskSelectable) {
        deskOpen = !deskOpen;
        document.getElementById("desk").style.display = deskOpen ? "block" : "none";
    };
}

Now the furniture can be selected and you can decorate and fill your div however you want!

Before I leave you, I promised I would show you a more robust way to make sure all your essential elements are loaded before startGame() is called and everything is drawn onto your canvas.

If you’ve been using the same browser this whole time, the images will be in your cache and easy to retrieve, but you might notice some problems if you try loading the page in an incognito window or different device, for example.

At the moment, we just have the following code to check if our wall has loaded:


if (wall.complete) {
    startGame();
} else {
    wall.addEventListener("load", startGame);
}

We're going to replace all that with the following:


const requiredImages = [sprite, floor, wall, divider];
let loadedCount = 0;
let started = false;
function checkAllLoaded() {
    if (!started && loadedCount === requiredImages.length) {
        started = true;
        startGame();
    }
}
requiredImages.forEach(img => {
    if (img.complete && img.naturalWidth !== 0) {
        loadedCount++;
    } else {
        img.addEventListener("load", () => {
            loadedCount++;
            checkAllLoaded();
        });
        img.addEventListener("error", () => {
            loadedCount++;
            checkAllLoaded();
        });
    }
});
    checkAllLoaded();

If you have any other larger image files that you’re having problems with, just add them to the list.

And now, we’re done! Check your code against mine: Javascript HTML CSS and click here for a demo.

I hope this was useful! Let me know if you make anything with this, I'd love to see!