Draw a sprite on an HTML canvas, move it by changing its state, and keep it inside the play area.
Why this matters
A prototype answers a design question: is this jump fun, is this enemy readable, does this loop hold attention for five minutes? The fastest way to answer it is often a single HTML file that runs in any browser. The <canvas> element gives you a blank bitmap you can draw on with JavaScript, with no engine, install or build step. Many jam games and pitch prototypes start exactly like this, and the habits you build here (state in plain objects, drawing as a function of that state) carry straight into Godot, Unity or any engine.
The canvas and its context
A canvas is an element with a fixed drawing size, set with the width and height attributes (not CSS, which only stretches the bitmap):
<canvas id="game" width="320" height="200"></canvas>
To draw, ask the canvas for a 2D rendering context. Every drawing call goes through it:
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#1e293b"; // background colour
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#f59e0b"; // the "sprite"
ctx.fillRect(40, 80, 24, 24); // x, y, width, height
The coordinate system starts at the top-left corner. x grows to the right and y grows downwards, which surprises people used to maths graphs. A rectangle at (40, 80) has its top-left corner 40 pixels from the left edge and 80 from the top.
Useful calls to know:
| call | what it does |
|---|---|
fillRect(x, y, w, h) | solid rectangle |
clearRect(x, y, w, h) | erase to transparent |
beginPath(), arc(x, y, r, 0, Math.PI * 2), fill() | a circle |
drawImage(img, x, y) | draw a loaded image (a real sprite) |
fillText("Score: 3", x, y) | text, styled with ctx.font |
State first, pixels second
The canvas has no memory of "objects". Once you draw a rectangle it is just coloured pixels. To move something you change its data and draw everything again. Keep the game's state in a plain object:
const player = { x: 40, y: 80, w: 24, h: 24, color: "#f59e0b" };
function drawFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // wipe the last frame
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.w, player.h);
}
function movePlayer(dx, dy) {
player.x += dx;
player.y += dy;
}
movePlayer(10, 0);
drawFrame(); // the square appears 10px further right
This split is the most important idea in the module. movePlayer changes state; drawFrame turns state into pixels. Your game logic never reads pixels back, so you can test it without looking at the screen, and you can swap a coloured square for real art later without touching the rules.
Keeping things on screen
Players find edges fast. Without a guard, the square slides off and never comes back. Clamping keeps a value inside a range:
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function movePlayer(dx, dy) {
player.x = clamp(player.x + dx, 0, canvas.width - player.w);
player.y = clamp(player.y + dy, 0, canvas.height - player.h);
}
Note the upper bound: canvas.width - player.w, because x is the left edge. If you clamp to canvas.width, the sprite can sit entirely off the right side.
Exposing state for debugging
During prototyping, put your state where you can poke it from DevTools:
window.game = { player, movePlayer, drawFrame };
Now you can type game.movePlayer(50, 0); game.drawFrame() in the console, or set game.player.color = "red" to test readability. The Do activity uses this same object to check your work.
Sharp pixels and sizing
If you scale the canvas with CSS (say width: 100%), the browser stretches the bitmap and pixel art goes blurry. For a crisp retro look keep the bitmap small and add image-rendering: pixelated; to the canvas's CSS. For non-pixel art, set the width/height attributes to the size you actually display.
Common mistakes
- Forgetting
clearRect. Each frame draws on top of the last, leaving a smear trail. - Setting size with CSS only. The default bitmap is 300×150, stretched to your CSS size.
- Thinking y goes up. Moving "up" means
dyis negative. - Drawing before setting
fillStyle. The colour applies to calls made after you set it. - Storing positions in the DOM or reading pixels to find objects. Keep a state object; draw from it.
Quick recap
<canvas width height>plusgetContext("2d")gives you a drawing surface.- The origin is top-left;
ygrows downwards. - Keep the game in a state object;
drawFrameclears and redraws from it. - Clamp positions with the sprite's size in mind.
- Exposing
window.gamemakes prototypes easy to debug and test.
Try it
Open the Do tab. You will finish movePlayer (with clamping) and drawFrame for a small canvas prototype.