In Part 4, we added wall wrapping and kids mode. Now we make the game accessible on every device. Most players will find your game on their phone — without touch controls, they cannot play at all. In this part, we add swipe-based direction controls that feel natural on mobile and make the canvas responsive.
Mobile browsers fire three main touch events: touchstart when a finger touches the screen, touchmove as it slides, and touchend when it lifts. For swipe detection, we only need touchstart and touchend.
let touchStartX = 0;
let touchStartY = 0;
document.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
}, { passive: true });
document.addEventListener('touchend', (e) => {
const touch = e.changedTouches[0];
const deltaX = touch.clientX - touchStartX;
const deltaY = touch.clientY - touchStartY;
handleSwipe(deltaX, deltaY);
}, { passive: true });
We record the finger position on touchstart, then calculate how far it moved on touchend. The difference (delta) tells us the swipe direction and distance.
{ passive: true } on touch listeners when you do not call preventDefault(). This tells the browser the event will not be cancelled, allowing smoother scrolling performance.
Compare the absolute values of deltaX and deltaY to determine if the swipe is horizontal or vertical. Then check the sign to get the exact direction.
function handleSwipe(deltaX, deltaY) {
const absDx = Math.abs(deltaX);
const absDy = Math.abs(deltaY);
if (absDx > absDy) {
// Horizontal swipe
if (deltaX > 0) {
changeDirection('right');
} else {
changeDirection('left');
}
} else {
// Vertical swipe
if (deltaY > 0) {
changeDirection('down');
} else {
changeDirection('up');
}
}
}
If the horizontal distance is greater than the vertical distance, it is a horizontal swipe. Otherwise, it is vertical. This simple comparison handles diagonal swipes by picking the dominant axis.
Tiny accidental touches should not change direction. Add a minimum distance threshold — if the finger moved less than 20 pixels, ignore it.
const SWIPE_THRESHOLD = 20; // minimum pixels
function handleSwipe(deltaX, deltaY) {
const absDx = Math.abs(deltaX);
const absDy = Math.abs(deltaY);
// Ignore tiny movements
if (absDx < SWIPE_THRESHOLD && absDy < SWIPE_THRESHOLD) {
return;
}
if (absDx > absDy) {
if (deltaX > 0) changeDirection('right');
else changeDirection('left');
} else {
if (deltaY > 0) changeDirection('down');
else changeDirection('up');
}
}
The changeDirection() function maps swipe names to direction vectors, with the same reversal prevention we use for keyboard input.
function changeDirection(dir) {
switch (dir) {
case 'up':
if (direction.y === 0)
nextDirection = { x: 0, y: -1 };
break;
case 'down':
if (direction.y === 0)
nextDirection = { x: 0, y: 1 };
break;
case 'left':
if (direction.x === 0)
nextDirection = { x: -1, y: 0 };
break;
case 'right':
if (direction.x === 0)
nextDirection = { x: 1, y: 0 };
break;
}
}
This is the same logic as the keyboard handler from Part 1, extracted into a shared function. Both keyboard and touch input call changeDirection(), so the reversal prevention works identically for both.
// Refactor keyboard handler to use the same function
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp': changeDirection('up'); break;
case 'ArrowDown': changeDirection('down'); break;
case 'ArrowLeft': changeDirection('left'); break;
case 'ArrowRight': changeDirection('right'); break;
}
});
On mobile, the canvas needs to fit the screen width. We scale it with CSS while keeping the internal resolution fixed. This gives us sharp rendering at any screen size.
function resizeCanvas() {
const maxWidth = Math.min(window.innerWidth - 16, 400);
canvas.style.width = maxWidth + 'px';
canvas.style.height = maxWidth + 'px';
}
// Resize on load and orientation change
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', resizeCanvas);
resizeCanvas();
The canvas internal size stays at 400×400 (or whatever your grid dictates). CSS width and height scale the display without affecting the coordinate system. This means all your drawing code works unchanged — the browser handles the scaling.
Some desktop UI elements do not make sense on mobile. Use CSS media queries to hide or rearrange them.
<style>
@media (max-width: 600px) {
.desktop-controls {
display: none;
}
.game-container {
padding: 4px;
}
canvas {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}
}
</style>
Hide keyboard instruction text (like "Use arrow keys") on mobile since it does not apply. Keep the score display visible — players always want to see their score. If you have a restart button, make it large enough to tap easily (at least 44×44 pixels, per Apple's Human Interface Guidelines).
Desktop browser DevTools have a device emulator, but it does not catch everything. Here is a practical testing checklist:
npx serve . and open your local IP on your phone// Prevent page scroll while playing
document.addEventListener('touchmove', (e) => {
if (!gameOver) {
e.preventDefault();
}
}, { passive: false });
{ passive: false } only on the touchmove listener where you call preventDefault(). This stops the page from scrolling during gameplay but allows normal scrolling when the game is not active.
With mobile controls in place, your Snake game works on every device. In the final part, we add sound effects — eat sounds, death sounds, and level-up jingles using the Web Audio API.
Continue to Part 6: Sound Effects →