0 Spicy Yoghurt | Last updated: 5 November 2026 | HTML5 game tutorial
Finish and publish your HTML5 game
Learn how to publish your HTML5 game on the web, on game portals, in the app stores and on Steam. But first, look back at the bee game you've built in this tutorial series and play a more polished version of it, complete with source code.
Part of the HTML5 game tutorial series
This is the last part. It looks back at the bee game from the part about sound effects and music, shows what it can grow into, and then helps you get your own game out into the world.
Show all partsHide all parts
Here's Pollen Rush, the bee game with a few extras. Collect the yellow pollen and stay away from the other bees. Click or tap the canvas to start:
Your own bee has a dashed ring around it. Steer with the arrow keys or WASD, or press and hold where you want to fly, with the mouse or your finger. Every bee that hits you costs one of your three lives. Collect pollen quickly, one grain after another, for a combo, and grab the golden pollen before it disappears. After every 10 grains, another bee joins the swarm.
Look back at what you've built
Pollen Rush is built from the same pieces as the game you made in this series. The series started with an overview of how to make an HTML5 game. Then every part added one piece:
- The canvas - Everything in the game is drawn on one canvas element, sharp on every screen. You learned how to set up an HTML5 canvas.
- Drawing - The pollen, the score and the buttons are shapes and text that you draw on the canvas with JavaScript.
- The game loop - Every frame, the game updates everything and draws it again. That's the JavaScript game loop.
- Animation - Some screens draw 60 frames per second, others 144 or more. So every bee moves a fixed number of pixels per second, not per frame, and the game runs at the same speed on every screen. That's how you make a smooth canvas animation.
- Collisions - A circle around every bee and every grain of pollen tells the game what touches what. The bees bounce off each other with the physics from collision detection.
- Sprites - The bees flap their wings with a sprite animation, and get dizzy when they bump into something.
- Controls - You can steer your bee with the arrow keys or WASD, the mouse or a finger. Learn how to add keyboard, mouse and touch controls.
- Game states - The start screen, the game itself, the pause menu and the game over screen are JavaScript game states.
- Sound - Every pickup, bump and hit has its own sound effect, and the music gets softer in the pause menu.
Want to see each part at work? The canvas below plays Pollen Rush by itself. Pick a part with the X-ray buttons, and the canvas shows what it does in the game:
With Collisions, for example, you see the circles that the game checks every frame. When two bees touch, their circles fill with red. With States, you see the game go from the start screen to playing, to game over and back.
What's new in Pollen Rush
Pollen Rush starts from the complete code of the part about sound effects and music, and gives the game a goal. The extras are built almost entirely from pieces you already know:
- Pollen - There are always four yellow dots on the field, each one a grain of pollen. A grain is an object with a position, a radius and an age. The game uses circleIntersect(), the function that checks whether two circles overlap, to see if your bee touches a grain. The game uses the age to make a new grain grow to full size, and to make a golden grain blink and disappear. Collect a grain, and a new one grows somewhere else, never right next to your bee.
- Combo - Collect the next grain within 2 seconds, and it's worth twice as much, then three times, up to five times. A bar under the score shows how much time you have left. A hit ends your combo.
- Golden pollen - Every 10 to 20 seconds, a bigger, golden grain appears. It's worth 50 points, but it starts to blink after 3 seconds and disappears after 5.
- Waves - After every 10 grains, a new wave starts. One more bee joins the swarm, a little faster than the others. A red ring warns you a second before the new bee appears.
- Screen shake - When a bee hits you, everything on the field shakes for a moment.
The screen shake is the smallest extra, and you can add it to any game. Before you draw, move the canvas a few pixels in a random direction, and let the shake fade out:
// When a bee hits you, in hitPlayer(): full strength
shake = 1;
// In draw(): move everything a few pixels in a random direction
context.save();
if (shake > 0) {
let size = 8 * shake;
context.translate(
(Math.random() * 2 - 1) * size,
(Math.random() * 2 - 1) * size
);
}
// ...draw the game objects here...
context.restore();
// Every frame: fade out in a third of a second
shake = Math.max(shake - 3 * secondsPassed, 0);shake is 1 right after a hit, and 0 when everything stands still. save() remembers how the canvas was set up, and translate() moves everything you draw after it. restore() puts the canvas back the way it was, so the score and the buttons you draw after it don't shake. Small effects like this, which make a game feel alive, are called game juice. The points that float up when you collect pollen are another example. You'll find all these extras in the complete code at the end of this page.
Get your game ready for release
A game that works on your own computer isn't finished yet. Before you share it, go through this list:
- Test on a real phone - The mobile view in your browser's developer tools doesn't show how fast your game runs on a phone, or how it feels to play with your thumbs. Check the controls, the sound and the screen size.
- Keep it small - Players leave when a game takes too long to load, and game portals have size limits. Make your images as small as you can, and use MP3 files for sound.
- Make it easy to start - Tell players what to do on the start screen, like "Collect the pollen. Dodge the bees.", and let the first click or tap start the game.
- Pause when the player leaves - Pause the game when the player switches tabs or clicks outside the game, with the visibilitychange and blur events, as shown in the part about game states.
- Credit what you use - Free images, sounds and music often come with a license that asks you to credit the creator. Put the names in your game or on your game's web page.
- Write a privacy policy - App stores ask for one. Say what your game saves and what it sends, even if that's nothing more than a high score in local storage.
- Prepare an icon and screenshots - Every store and portal asks for them, along with a short description. A short video or GIF of your game helps people decide to play it.
Put your game on itch.io
itch.io is the easiest place to start. It's free, and you can upload an HTML5 game in a few minutes:
- Step 1 - Put your game in a folder, and name its HTML file index.html. Load your images and sounds with relative paths, like img/bee.png, not /img/bee.png. itch.io runs your game from a folder of its own, and a path that starts with a slash looks in the wrong place.
- Step 2 - Zip the folder. A game that's just one HTML file can also go up on its own, without a zip.
- Step 3 - Create a new project on itch.io, mark it as an HTML game and upload the zip.
- Step 4 - Choose how your game shows up: embedded on the page at a size you pick, or in full screen after the player clicks. itch.io adds a full-screen button for you. If your game supports touch, turn on Mobile Friendly.
itch.io doesn't take a fixed cut. You decide how much of every payment goes to itch.io, and it's 10% unless you change it. For a game that runs in the browser, itch.io only lets you ask for donations. To sell your game, offer it as a download.
Want your game on a site of your own instead? The tutorial about how to make an HTML5 game shows a few other places to put it, like GitHub Pages.
Publish on game portals like Poki and CrazyGames
Game portals have millions of players, and they share their ad revenue with you. But they're picky:
- Poki - Every game on Poki is hand-picked. You apply with a link to a playable version, or with a video. Your game shows ads through the Poki SDK, a script from Poki that you add to your game. What the player loads before the game starts should be 5 MB at most, and the whole game 8 MB at most. Poki usually asks for web exclusivity for a few years. That means your game can't be on other game sites, and maybe not on itch.io or your own site either, so read the deal before you sign. The app stores and Steam stay open to you.
- CrazyGames - Anyone can submit a game. Your game starts with a basic launch, without ads. After at least a week and 500 plays, CrazyGames decides whether it gets a full launch, with ads through their SDK. Your whole game can be up to 50 MB, or 20 MB if you want it to show up on the CrazyGames homepage on phones. With their SDK, your game can be up to 250 MB, and those limits only apply to what the player loads before the game starts. The rest can load while they play.
Portals turn down most games, so don't take a no personally. Improve your game and try again, or try a different portal.
Make your game installable as a web app
Before you look at the app stores, there's an easier way to get your game onto a phone's home screen. Turn it into a progressive web app, or PWA. The browser can then install your game like an app. It gets its own icon, and it opens without the address bar.
Your game needs to be on a site with HTTPS, like GitHub Pages, and it needs a manifest. For testing, a local server works too, but opening the HTML file by double-clicking it doesn't. The manifest is a small file with the name of your game, its icons and how it should open:
{
"name": "Pollen Rush",
"short_name": "Pollen Rush",
"start_url": "./",
"display": "fullscreen",
"background_color": "#ffffff",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Make the two icons it lists, 192 and 512 pixels square. start_url is the page that opens, and with display set to fullscreen, your game opens without any browser bars. background_color is the color of the screen while your game loads.
Save it as manifest.json in the same folder as your index.html, and link to it in the <head> of your game:
<link rel="manifest" href="manifest.json">To check it, open your game in Chrome and open the developer tools. Under Application, then Manifest, Chrome lists anything that's still missing. Once it's all there, an install button shows up in the address bar.
Want your game to work without an internet connection, too? Then add a service worker. That's a script that saves your game's files in the browser and serves them from there, so the game also works without a connection:
// sw.js: your game's files, saved under a version name
const cacheName = 'pollen-rush-v1';
const files = [
'./',
'index.html',
'img/bee-sprite-sheet.png',
'sound/bee-music.mp3'
];
// Save all files when the service worker is installed
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(files);
})
);
});
// Remove the files of older versions of your game
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (names) {
let old = names.filter(function (name) {
return name.startsWith('pollen-rush-') &&
name != cacheName;
});
return Promise.all(old.map(function (name) {
return caches.delete(name);
}));
})
);
});
// Answer with a saved file, or else ask the network
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
})
);
});'./' is the folder itself, the address that players usually open. Save the script as sw.js in the same folder as your index.html, and register it in init(), the function that starts your game when the page has loaded:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}Add every image and sound your game uses to the files array. They have to be your own files, in your game's folder, not files on another site. The complete code at the end of this page loads its images and sounds from spicyyoghurt.com, so replace them with your own files first. And whenever you change a file, change the version in cacheName too. Then the browser fetches the new files on the next visit, and throws away the old ones.
Publish your game on Google Play
Google Play accepts a web app as an Android app, through a Trusted Web Activity. Your game stays on your website, and the app opens it in full screen. Bubblewrap, a tool from Google that you run in a terminal, builds the app from your manifest. Like Capacitor and Electron further down this page, it needs Node.js, which comes with the npm command:
npm install -g @bubblewrap/cli
bubblewrap init --manifest=https://example.com/game/manifest.json
bubblewrap buildbubblewrap init asks a few questions, and can install the tools it needs by itself. bubblewrap build then makes an app bundle, an .aab file, that you upload to Google Play. Would you rather click than type? PWABuilder does the same in your browser.
The app only opens your game in full screen if your website proves that the app is yours. Put a file named assetlinks.json in a folder named .well-known at the root of your site. It holds the fingerprint of the key your app is signed with. Google Play signs your app with its own key, so put the fingerprint of that key in the file. You'll find it in the Play Console, under App integrity. Bubblewrap can write the file for you, with the fingerprint you give it.
A developer account on Google Play costs a one-time fee of $25. Is it a new personal account, not a company account? Then Google wants at least 12 testers to try your game for 14 days in a row before you can publish it for everyone. So ask your friends early.
Publish your game on the App Store
Apple doesn't accept an app that only opens a website. Your game has to be inside the app. Capacitor puts your HTML, JavaScript, images and sounds into an iPhone app. Put your game in a folder named www, and run this in the folder that contains it:
npm install @capacitor/core
npm install -D @capacitor/cli
npx cap init
npm install @capacitor/ios
npx cap add ios
npx cap open iosnpx cap init asks for the name of your app, an ID like com.example.pollenrush and the folder with your game: www. npx cap open ios opens the project in Xcode, Apple's app for making apps. From there, you test the game on your iPhone and submit it to the App Store. Changed your game? Run npx cap copy ios to copy the new files into the app.
You need a Mac with Xcode, and an Apple Developer Program membership, which costs $99 a year. Apple also checks that every app is more than a website in a box. A game that works offline, with all its files inside the app, has a head start. Make sure it doesn't send players to a website, and that all of it works offline.
One more thing: an iPhone's silent mode also silences the Web Audio API. If you want your game to play sound in silent mode too, set navigator.audioSession.type to 'playback', as explained in the part about sound effects and music.
Publish your game on Steam
Steam sells games for Windows, Mac and Linux, and an HTML5 game can be one of them.
To turn your game into a desktop program, wrap it in Electron. Electron opens your index.html in a window of its own. Install Electron with npm install electron --save-dev. Then put this main.js in the same folder as your index.html. It opens a 750 by 400 window, the size of the canvas, with your game in it:
const {app, BrowserWindow} = require('electron/main');
function createWindow() {
let win = new BrowserWindow({width: 750, height: 400});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
// Quit when the window closes, except on a Mac
app.on('window-all-closed', function () {
if (process.platform != 'darwin') {
app.quit();
}
});npx electron . starts your game in its window. To turn it into a program you can upload to Steam, add Electron Forge to your project, as its guide shows, and run npm run make. Each operating system needs its own build, usually made on that system: the Windows version on Windows, and the Mac version on a Mac. For achievements and the Steam overlay, steamworks.js connects your game to Steam. It's not an official Valve tool; it's a free project by other developers.
Steam charges $100 per game. You don't get it back right away, but once your game has earned $1,000, Steam adds it to your payout. After you pay, it takes at least 21 days before you can release your game, and your store page has to say Coming Soon for at least two weeks before the release.
Which platform should you start with?
Start small. Put your game on itch.io, and share the link with your friends. Watch them play, and fix what they get stuck on. Does it play well on a phone? Then try a game portal. The app stores and Steam take more time and money, so save them for a game that people already like to play.
Common questions about publishing your game
How much does it cost to publish a game?
Nothing on itch.io, GitHub Pages or a game portal. A developer account on Google Play costs a one-time fee of $25. The App Store costs $99 a year. Steam charges $100 per game, and adds it to your payout once your game has earned $1,000.
Do I need a Mac to publish on the App Store?
Yes. Xcode, the app that builds iPhone apps, only runs on a Mac. Don't have one? You can rent a Mac in the cloud for as long as you need it.
Do I need to rewrite my game for phones?
No. If your game works with touch and fits the screen, it runs in the browser on a phone, and the app stores get the same code. You set that up in the parts about the canvas and the controls.
Can I publish the same game in more than one place?
Yes. itch.io, the app stores and Steam are fine with that. A game portal like Poki may ask for web exclusivity, but that still leaves the app stores and Steam open to you.
Can I use the code, images and sounds from this series in my own game?
You can use the code, change it, and even sell a game that uses it, as long as you credit Spicy Yoghurt, for example in a comment in your code. That doesn't include the bee images and the sounds, so make your own, or use free ones with a license that allows it. The details are in our terms.
The complete Pollen Rush code
Here's the complete code of Pollen Rush. It builds on the code from the part about sound effects and music, and it loads the sprite sheet and the sounds from spicyyoghurt.com. Paste it into an HTML file and open it in your browser:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pollen Rush | Spicy Yoghurt</title>
<style>
#canvas {
width: 750px;
height: 400px;
border: 1px solid lightgray;
}
</style>
</head>
<body>
<canvas
id="canvas"
width="750"
height="400"
tabindex="0"
>
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
'use strict';
// Define the edges of the canvas
const canvasWidth = 750;
const canvasHeight = 400;
// The sprite sheet of the bee
const spriteUrl =
'https://spicyyoghurt.com/img/tutorials/' +
'develop_a_html5_game/bee-sprite-sheet.png';
// The sound files, with a volume for each sound and how much
// its pitch can vary every time it plays (0.1 is 10%)
const soundUrl =
'https://spicyyoghurt.com/img/tutorials/' +
'develop_a_html5_game/';
const soundFiles = {
start: {file: 'bee-start.mp3', volume: 0.5, vary: 0},
pollen: {file: 'bee-pollen.mp3', volume: 0.5, vary: 0},
hit: {file: 'bee-hit.mp3', volume: 0.7, vary: 0.1},
buzz: {file: 'bee-buzz.mp3', volume: 0.4, vary: 0.1},
bump: {file: 'bee-bump.mp3', volume: 0.3, vary: 0.15},
gameOver: {file: 'bee-game-over.mp3', volume: 0.6, vary: 0},
best: {file: 'bee-best.mp3', volume: 0.6, vary: 0},
music: {file: 'bee-music.mp3', volume: 0.35, vary: 0}
};
let canvas;
let context;
let oldTimeStamp = 0;
let gameObjects = [];
let player;
// The game keys, by event.code
const gameKeys = [
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space'
];
// The keys that are down right now
let keys = {};
// The mouse or finger on the canvas
let pointer = {x: 0, y: 0, isDown: false};
// How many seconds the game has been in this state
let stateTime = 0;
let score = 0;
let lives = 3;
let highScore = 0;
let isNewHighScore = false;
// The pollen on the field
let pollen = [];
const pollenOnField = 4;
// Collect pollen quickly, one after another, for a combo:
// the next one is worth 2 times as much, then 3 times
let combo = 1;
let comboTimer = 0;
const comboTime = 2;
const maxCombo = 5;
// Seconds until the next golden grain
let goldenTimer = 15;
// After every 10 pollen, a new wave: one more bee joins
// the swarm, a little faster than the others
let wave = 1;
let pollenCount = 0;
const pollenPerWave = 10;
let beeSpeed = 100;
const maxBees = 25;
// The spot where the next bee will appear, and when
let nextBee = null;
let spawnTimer = 0;
// How long the name of a new wave stays on the screen
let waveTextTime = 0;
// How hard the screen shakes, from 0 to 1
let shake = 0;
// The points that pop up where you score them
let popups = [];
// The pause button in the top-right corner
const pauseButton = {x: 696, y: 14, width: 40, height: 40};
// The buttons on the pause screen
const menuButtons = [
{
text: 'Resume',
x: 285, y: 150, width: 180, height: 46,
onClick: resumeGame
},
{
text: 'Restart',
x: 285, y: 212, width: 180, height: 46,
onClick: startGame
},
{
text: 'Quit',
x: 285, y: 274, width: 180, height: 46,
onClick: quitGame
}
];
// The mute button, to the left of the pause button
const muteButton = {x: 646, y: 14, width: 40, height: 40};
// One AudioContext for all the sound in the game
let audioContext;
let masterGain;
let musicGain;
let musicFilter;
// The decoded sounds by name, and the music that's playing
let sounds = {};
let music = null;
let isMuted = false;
// When each sound can play again, in seconds
let soundReadyAt = {};
// Every state has its own update(), draw() and controls.
// enter() and exit() run when the game switches states.
const states = {
start: {
enter() {
// Back on the start screen, the music stops
stopMusic();
},
update(secondsPassed) {
// The bees already fly around behind the title
updateObjects(secondsPassed);
},
draw() {
drawStartScreen();
},
onKeyDown(code) {
if (isStartKey(code)) {
startGame();
}
},
onPointerDown(event) {
startGame();
}
},
playing: {
enter() {
// Your finger steers the bee instead of scrolling
canvas.style.touchAction = 'none';
},
exit() {
// On every other screen, the page scrolls again
canvas.style.touchAction = 'auto';
},
update(secondsPassed) {
updateObjects(secondsPassed);
updatePollen(secondsPassed);
updateSpawning(secondsPassed);
updateEffects(secondsPassed);
},
draw() {
drawHud();
drawPauseButton();
},
onKeyDown(code) {
if (isPauseKey(code)) {
pauseGame();
}
},
onPointerDown(event) {
if (isOnButton(pauseButton)) {
pauseGame();
} else {
// Fly toward the mouse or finger while it's down
pointer.isDown = true;
canvas.setPointerCapture(event.pointerId);
}
}
},
paused: {
enter() {
// Soft, muffled music while the game is paused
muffleMusic(true);
},
exit() {
// And back to normal when the pause ends
muffleMusic(false);
},
update(secondsPassed) {
// Everything stands still
},
draw() {
drawHud();
drawPauseScreen();
},
onKeyDown(code) {
if (isPauseKey(code)) {
resumeGame();
}
},
onPointerDown(event) {
// Click a button on the pause screen
for (let i = 0; i < menuButtons.length; i++) {
if (isOnButton(menuButtons[i])) {
menuButtons[i].onClick();
}
}
}
},
gameover: {
enter() {
// Keep the best score, and save it for next time
isNewHighScore = Math.floor(score) > highScore;
if (isNewHighScore) {
highScore = Math.floor(score);
localStorage.setItem(
'pollenRushHighScore', highScore
);
}
// Stop the music and play a short tune
stopMusic();
playSound(isNewHighScore ? 'best' : 'gameOver');
},
update(secondsPassed) {
// Everything stands still, but the shake
// and the points fade out
updateEffects(secondsPassed);
},
draw() {
drawHud();
drawGameOverScreen();
},
onKeyDown(code) {
// Wait a second, so you don't miss your score
if (stateTime > 1 && isStartKey(code)) {
startGame();
}
},
onPointerDown(event) {
if (stateTime > 1) {
startGame();
}
}
}
};
// The state the game is in right now
let state = states.start;
class GameObject {
constructor(context, x, y, vx, vy, mass) {
this.context = context;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.mass = mass;
this.restitution = 0.9;
this.isColliding = false;
}
}
class Bee extends GameObject {
// One sprite sheet for all bees, with 4 columns and 2 rows
static sprite;
static numColumns = 4;
static numRows = 2;
static frameWidth = 128;
static frameHeight = 128;
// One animation for every row of the sprite sheet
static animations = {
fly: {row: 0, fps: 20, loop: true},
bump: {row: 1, fps: 10, loop: false}
};
constructor(context, x, y, vx, vy) {
super(context, x, y, vx, vy, 1);
// Keep flying at the same speed after a bump
this.restitution = 1;
// The hitbox is the round body of the bee
this.radius = 15;
// Face the direction of the velocity
this.angle = Math.atan2(vy, vx);
// Start at a random frame, so the bees don't flap in sync
this.animation = Bee.animations.fly;
this.currentFrame =
Math.floor(Math.random() * Bee.numColumns);
this.frameTimer = 0;
}
play(name) {
this.animation = Bee.animations[name];
this.currentFrame = 0;
this.frameTimer = 0;
}
bump() {
// Don't start over while the bee is still dizzy
if (this.animation != Bee.animations.bump) {
this.play('bump');
}
}
update(secondsPassed) {
// Move at the current velocity
this.x += this.vx * secondsPassed;
this.y += this.vy * secondsPassed;
// Calculate the angle, in radians
this.angle = Math.atan2(this.vy, this.vx);
// Pick the next frame when it's due
this.frameTimer += secondsPassed;
while (this.frameTimer >= 1 / this.animation.fps) {
this.frameTimer -= 1 / this.animation.fps;
this.currentFrame++;
if (this.currentFrame == Bee.numColumns) {
if (this.animation.loop) {
this.currentFrame = 0;
} else {
this.play('fly');
}
}
}
}
draw() {
// Save the context, move it to the bee and rotate it
this.context.save();
this.context.translate(this.x, this.y);
this.context.rotate(this.angle);
// Draw the image around (0, 0), the middle of the body
this.context.drawImage(
Bee.sprite,
this.currentFrame * Bee.frameWidth,
this.animation.row * Bee.frameHeight,
Bee.frameWidth, Bee.frameHeight,
-28, -32, 64, 64
);
// Put the context back the way it was
this.context.restore();
}
}
class Player extends Bee {
constructor(context, x, y) {
super(context, x, y, 0, 0);
// Heavier, so you push the other bees away
this.mass = 3;
// A bit bigger than the other bees, and so is the hitbox
this.scale = 1.25;
this.radius = 15 * this.scale;
// Pixels per second
this.speed = 200;
// How far the dashes of the ring have moved
this.dashOffset = 0;
// Seconds until a bee can hurt you again
this.invincibleTime = 0;
}
update(secondsPassed) {
this.invincibleTime -= secondsPassed;
// Move the dashes 20 pixels per second, so the ring turns
this.dashOffset -= 20 * secondsPassed;
// Fly in the direction of the controls
let direction = getDirection(this);
this.vx = direction.x * this.speed;
this.vy = direction.y * this.speed;
// Keep facing the same way when you stop
let angle = this.angle;
super.update(secondsPassed);
if (direction.x == 0 && direction.y == 0) {
this.angle = angle;
}
}
draw() {
// Blink after a hit, while nothing can hurt you
let blink = Math.floor(this.invincibleTime * 8) % 2;
if (this.invincibleTime > 0 && blink == 1) {
this.context.globalAlpha = 0.3;
}
// A dashed ring that turns,
// so you can spot your own bee in the swarm
this.context.strokeStyle = '#0099b0';
this.context.lineWidth = 3;
this.context.setLineDash([8, 7]);
this.context.lineDashOffset = this.dashOffset;
this.context.beginPath();
this.context.arc(this.x, this.y, 34, 0, 2 * Math.PI);
this.context.stroke();
this.context.setLineDash([]);
// Draw the bee bigger, scaled around its middle
this.context.save();
this.context.translate(this.x, this.y);
this.context.scale(this.scale, this.scale);
this.context.translate(-this.x, -this.y);
super.draw();
this.context.restore();
this.context.globalAlpha = 1;
}
}
window.onload = init;
function init() {
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Set up the sound, and check if you muted it last time
createAudio();
isMuted = localStorage.getItem('beeMuted') == 'true';
masterGain.gain.value = isMuted ? 0 : 1;
// The keyboard, only when the canvas has focus
canvas.addEventListener('keydown', onKeyDown);
canvas.addEventListener('keyup', onKeyUp);
canvas.addEventListener('blur', releaseAllKeys);
canvas.focus();
// The mouse, a finger or a pen
canvas.addEventListener('pointerdown', onPointerDown);
canvas.addEventListener('pointermove', updatePointer);
canvas.addEventListener('pointerup', onPointerUp);
canvas.addEventListener('pointercancel', onPointerUp);
// Pause when you click somewhere else or switch tabs
canvas.addEventListener('blur', pauseGame);
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
pauseGame();
// A hidden tab stops the game loop, but not the sound
audioContext.suspend();
} else if (state != states.start) {
audioContext.resume();
}
});
// The best score from an earlier visit, or 0
highScore =
Number(localStorage.getItem('pollenRushHighScore'));
// Show the start screen when the sprite sheet
// and the sounds have loaded
let loading = [loadImage(spriteUrl), loadSounds()];
Promise.all(loading).then(function (results) {
Bee.sprite = results[0];
resetGame();
window.requestAnimationFrame(gameLoop);
});
}
function loadImage(src) {
return new Promise(function (resolve, reject) {
let img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = reject;
img.src = src;
});
}
function createAudio() {
audioContext = new AudioContext();
// Every sound goes through the master volume to the speakers
masterGain = audioContext.createGain();
masterGain.connect(audioContext.destination);
// The music has its own volume, and a filter
// for when the game is paused
musicGain = audioContext.createGain();
musicGain.gain.value = soundFiles.music.volume;
musicFilter = audioContext.createBiquadFilter();
musicFilter.type = 'lowpass';
musicFilter.frequency.value = 20000;
musicGain.connect(musicFilter);
musicFilter.connect(masterGain);
}
function loadSound(url) {
// Download the file and decode it into an AudioBuffer
return fetch(url)
.then(function (response) {
return response.arrayBuffer();
})
.then(function (data) {
return audioContext.decodeAudioData(data);
})
.catch(function () {
// No sound, but the game still works
return null;
});
}
function loadSounds() {
let names = Object.keys(soundFiles);
let loading = names.map(function (name) {
return loadSound(soundUrl + soundFiles[name].file);
});
// Keep each decoded sound under its name
return Promise.all(loading).then(function (buffers) {
for (let i = 0; i < names.length; i++) {
sounds[names[i]] = buffers[i];
}
});
}
function playSound(name, x = canvasWidth / 2, pitch = 1) {
let now = audioContext.currentTime;
// Skip it if it didn't load, or if it played a moment ago
if (!sounds[name] || now < (soundReadyAt[name] || 0)) {
return;
}
soundReadyAt[name] = now + 0.1;
// A new source every time, because a source only plays once
let source = audioContext.createBufferSource();
source.buffer = sounds[name];
// A slightly different pitch every time
let change = (Math.random() * 2 - 1) * soundFiles[name].vary;
source.playbackRate.value = (1 + change) * pitch;
// The volume of this sound
let gain = audioContext.createGain();
gain.gain.value = soundFiles[name].volume;
// Left or right, based on where it happens on the canvas
let panner = audioContext.createStereoPanner();
panner.pan.value = (x - canvasWidth / 2) / canvasWidth;
source.connect(gain);
gain.connect(panner);
panner.connect(masterGain);
source.start();
}
function startMusic() {
// Only one track at a time, so stop the old one first
stopMusic();
if (!sounds.music) {
return;
}
music = audioContext.createBufferSource();
music.buffer = sounds.music;
music.loop = true;
music.connect(musicGain);
music.start();
}
function stopMusic() {
if (music) {
music.stop();
music = null;
}
}
function muffleMusic(isMuffled) {
let now = audioContext.currentTime;
let volume = soundFiles.music.volume;
let frequency = 20000;
// Half as loud, and only the low notes
if (isMuffled) {
volume /= 2;
frequency = 600;
}
// Change smoothly instead of all at once
musicGain.gain.setTargetAtTime(volume, now, 0.1);
musicFilter.frequency.setTargetAtTime(frequency, now, 0.1);
}
function toggleMute() {
isMuted = !isMuted;
localStorage.setItem('beeMuted', isMuted);
// Fade out quickly, so you don't hear a click
let volume = isMuted ? 0 : 1;
masterGain.gain.setTargetAtTime(
volume, audioContext.currentTime, 0.02
);
}
function resizeCanvas() {
// One canvas pixel for every pixel of the screen
let ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(canvas.clientWidth * ratio);
canvas.height = Math.round(canvas.clientHeight * ratio);
// Keep drawing in game coordinates (750 x 400)
context.setTransform(
canvas.width / canvasWidth, 0,
0, canvas.height / canvasHeight,
0, 0
);
}
function resetGame() {
// Your own bee, in the middle of the canvas
player = new Player(context, 375, 200);
// A small swarm, not too close to you
gameObjects = [];
for (let i = 0; i < 6; i++) {
let spot = findSpawnSpot();
gameObjects.push(createBee(spot.x, spot.y));
}
gameObjects.push(player);
// Fresh pollen on the field, already full-grown
pollen = [];
for (let i = 0; i < pollenOnField; i++) {
let p = createPollen(false);
p.age = 1;
pollen.push(p);
}
// Everything else back to how it was at the start
score = 0;
lives = 3;
isNewHighScore = false;
combo = 1;
comboTimer = 0;
goldenTimer = 15;
wave = 1;
pollenCount = 0;
beeSpeed = 100;
nextBee = null;
waveTextTime = 0;
shake = 0;
popups = [];
}
function findSpawnSpot() {
let x;
let y;
let dx;
let dy;
// Try random spots until one is far enough away from you
do {
x = 50 + Math.random() * 650;
y = 50 + Math.random() * 300;
dx = x - player.x;
dy = y - player.y;
} while (dx * dx + dy * dy < 200 * 200);
return {x: x, y: y};
}
function createBee(x, y) {
// A new bee flies at the speed of the current wave
let angle = Math.random() * 2 * Math.PI;
return new Bee(
context, x, y,
Math.cos(angle) * beeSpeed, Math.sin(angle) * beeSpeed
);
}
function createPollen(isGolden) {
// A random spot, not too close to your bee
let spot = findSpawnSpot();
return {
x: spot.x,
y: spot.y,
radius: isGolden ? 14 : 9,
isGolden: isGolden,
age: 0
};
}
function changeState(newState) {
// Let the old state finish, and the new state begin
if (state.exit) {
state.exit();
}
state = newState;
stateTime = 0;
if (state.enter) {
state.enter();
}
}
function startGame() {
// The first click or key press also turns on the sound
audioContext.resume();
resetGame();
changeState(states.playing);
playSound('start');
startMusic();
}
function pauseGame() {
// Only a running game can be paused
if (state == states.playing) {
changeState(states.paused);
}
}
function resumeGame() {
changeState(states.playing);
}
function quitGame() {
changeState(states.start);
}
function onKeyDown(event) {
keys[event.code] = true;
// Don't let the game keys scroll the page
if (gameKeys.includes(event.code)) {
event.preventDefault();
}
// A key that you hold down only counts once
if (event.repeat) {
return;
}
// M turns the sound on and off on every screen
if (event.code == 'KeyM') {
toggleMute();
return;
}
// The current state decides what the key does
state.onKeyDown(event.code);
}
// Space or Enter starts the game
function isStartKey(code) {
return code == 'Space' || code == 'Enter';
}
// P or Escape pauses or resumes the game
function isPauseKey(code) {
return code == 'KeyP' || code == 'Escape';
}
function onKeyUp(event) {
keys[event.code] = false;
}
function releaseAllKeys() {
// The game can't see keys being released anymore
keys = {};
}
function onPointerDown(event) {
updatePointer(event);
// The mute button works on every screen
if (isOnButton(muteButton)) {
toggleMute();
return;
}
// The current state decides what the click or tap does
state.onPointerDown(event);
}
function onPointerUp() {
pointer.isDown = false;
// On a touchscreen, the browser only allows sound
// once the finger lifts off the screen
audioContext.resume();
}
function updatePointer(event) {
// The position on the canvas, in screen pixels
let rect = canvas.getBoundingClientRect();
let screenX = event.clientX - rect.left;
let screenY = event.clientY - rect.top;
// Convert it to game coordinates
pointer.x = screenX * canvasWidth / rect.width;
pointer.y = screenY * canvasHeight / rect.height;
}
function getDirection(obj) {
let x = 0;
let y = 0;
// The arrow keys or WASD
if (keys.ArrowLeft || keys.KeyA) {
x -= 1;
}
if (keys.ArrowRight || keys.KeyD) {
x += 1;
}
if (keys.ArrowUp || keys.KeyW) {
y -= 1;
}
if (keys.ArrowDown || keys.KeyS) {
y += 1;
}
// Hold the mouse or a finger down to fly toward it
if (pointer.isDown) {
x = pointer.x - obj.x;
y = pointer.y - obj.y;
// Stop when you get there
if (x * x + y * y < 10 * 10) {
x = 0;
y = 0;
}
}
// Give the direction a length of 1, so you fly
// just as fast in every direction
let length = Math.sqrt(x * x + y * y);
if (length > 0) {
x /= length;
y /= length;
}
return {x: x, y: y};
}
function isOnButton(button) {
return pointInRect(
pointer.x, pointer.y,
button.x, button.y, button.width, button.height
);
}
function pointInRect(px, py, x, y, w, h) {
return px >= x && px <= x + w && py >= y && py <= y + h;
}
function gameLoop(timeStamp) {
// Calculate how much time has passed, capped at a maximum
let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
oldTimeStamp = timeStamp;
secondsPassed = Math.min(secondsPassed, 0.1);
stateTime += secondsPassed;
state.update(secondsPassed);
draw();
window.requestAnimationFrame(gameLoop);
}
function updateObjects(secondsPassed) {
// Loop over all game objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].update(secondsPassed);
}
detectCollisions();
detectEdgeCollisions();
}
function updatePollen(secondsPassed) {
// The combo ends when you take too long
comboTimer -= secondsPassed;
if (comboTimer <= 0) {
combo = 1;
}
// Now and then, a golden grain
goldenTimer -= secondsPassed;
if (goldenTimer <= 0) {
pollen.push(createPollen(true));
goldenTimer = 10 + Math.random() * 10;
}
// Loop backward, so removing one doesn't skip the next
for (let i = pollen.length - 1; i >= 0; i--) {
let p = pollen[i];
p.age += secondsPassed;
// A golden grain is gone after 5 seconds
if (p.isGolden && p.age > 5) {
pollen.splice(i, 1);
continue;
}
// Collect it when your bee touches it
if (circleIntersect(
player.x, player.y, player.radius,
p.x, p.y, p.radius
)) {
pollen.splice(i, 1);
collectPollen(p);
}
}
}
function collectPollen(p) {
// 10 points, or 50 for a golden one, times the combo
let points = (p.isGolden ? 50 : 10) * combo;
score += points;
popups.push({
x: p.x, y: p.y, text: '+' + points,
isGolden: p.isGolden, age: 0
});
// A higher note for every step of the combo
playSound('pollen', p.x, 1 + (combo - 1) * 0.12);
combo = Math.min(combo + 1, maxCombo);
comboTimer = comboTime;
// A new grain for every normal one you collect
if (!p.isGolden) {
pollen.push(createPollen(false));
pollenCount++;
if (pollenCount % pollenPerWave == 0) {
startWave();
}
}
}
function startWave() {
wave++;
waveTextTime = 2;
beeSpeed += 10;
// Stop adding bees when the swarm is big enough
if (gameObjects.length > maxBees) {
return;
}
// Pick a spot one second early, to warn the player
nextBee = findSpawnSpot();
spawnTimer = 1;
// A buzz from the side where the bee will appear
playSound('buzz', nextBee.x);
}
function updateSpawning(secondsPassed) {
if (!nextBee) {
return;
}
spawnTimer -= secondsPassed;
if (spawnTimer <= 0) {
// Add it first, so it's drawn under your bee
gameObjects.unshift(createBee(nextBee.x, nextBee.y));
nextBee = null;
}
}
function updateEffects(secondsPassed) {
// The shake fades out in a third of a second
shake = Math.max(shake - 3 * secondsPassed, 0);
waveTextTime -= secondsPassed;
// The points float up for a second, then disappear
for (let i = popups.length - 1; i >= 0; i--) {
popups[i].age += secondsPassed;
if (popups[i].age > 1) {
popups.splice(i, 1);
}
}
}
function hitPlayer() {
// Only while playing, and not right after a hit
if (state != states.playing || player.invincibleTime > 0) {
return;
}
lives--;
combo = 1;
shake = 1;
if (lives == 0) {
changeState(states.gameover);
return;
}
playSound('hit', player.x);
// Two seconds to get away
player.invincibleTime = 2;
}
function draw() {
// Clear the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Shake the game: move everything a few pixels
// in a random direction
context.save();
if (shake > 0) {
let size = 8 * shake;
context.translate(
(Math.random() * 2 - 1) * size,
(Math.random() * 2 - 1) * size
);
}
// The warning, the pollen, all game objects and the points
drawWarning();
drawPollen();
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].draw();
}
drawPopups();
context.restore();
// Draw the screen of the current state on top
state.draw();
// The mute button on top of every screen
drawMuteButton();
}
function drawWarning() {
if (!nextBee) {
return;
}
// A red ring that shrinks until the bee appears
context.strokeStyle = '#ff8080';
context.lineWidth = 3;
context.beginPath();
context.arc(
nextBee.x, nextBee.y, 15 + spawnTimer * 30, 0, 2 * Math.PI
);
context.stroke();
}
function drawPollen() {
for (let i = 0; i < pollen.length; i++) {
let p = pollen[i];
// Grow in a quarter of a second
let radius = p.radius * Math.min(p.age * 4, 1);
// A golden grain blinks in its last 2 seconds
let blink = Math.floor(p.age * 8) % 2;
if (p.isGolden && p.age > 3 && blink == 1) {
continue;
}
// A soft glow around a yellow dot
context.fillStyle = 'rgba(255, 211, 77, 0.3)';
context.beginPath();
context.arc(p.x, p.y, radius * 1.8, 0, 2 * Math.PI);
context.fill();
context.fillStyle = p.isGolden ? '#ffb300' : '#ffd34d';
context.strokeStyle = '#e0a000';
context.lineWidth = 2;
context.beginPath();
context.arc(p.x, p.y, radius, 0, 2 * Math.PI);
context.fill();
context.stroke();
}
}
function drawPopups() {
for (let i = 0; i < popups.length; i++) {
let popup = popups[i];
// Float up 40 pixels and fade out
context.globalAlpha = 1 - popup.age;
context.fillStyle =
popup.isGolden ? '#e08a00' : '#303030';
drawText(
popup.text, popup.x, popup.y - 20 - 40 * popup.age,
'bold 20px Arial'
);
}
context.globalAlpha = 1;
}
function drawHud() {
// The score in the top-left corner
context.font = 'bold 24px Arial';
context.textAlign = 'left';
context.textBaseline = 'middle';
context.fillStyle = '#303030';
context.fillText('Score: ' + Math.floor(score), 20, 30);
// The combo, with a bar that runs out
if (combo > 1) {
context.fillText('COMBO x' + combo, 160, 30);
context.fillStyle = '#ffd34d';
context.fillRect(
160, 46, 110 * comboTimer / comboTime, 6
);
}
// The name of a new wave, in the middle of the screen
if (waveTextTime > 0) {
context.fillStyle = '#303030';
drawText('WAVE ' + wave, 375, 200, 'bold 40px Arial');
}
// A small bee for every life you have left
for (let i = 0; i < lives; i++) {
context.drawImage(
Bee.sprite,
0, 0, Bee.frameWidth, Bee.frameHeight,
8 + i * 40, 42, 52, 52
);
}
}
function drawPauseButton() {
// Two bars in the top-right corner
let x = pauseButton.x;
let y = pauseButton.y;
context.fillStyle = '#303030';
context.fillRect(x + 10, y + 8, 7, 24);
context.fillRect(x + 23, y + 8, 7, 24);
}
function drawMuteButton() {
let x = muteButton.x;
let y = muteButton.y;
// White on the dark screens, dark on the others
let isDark =
state == states.paused || state == states.gameover;
context.fillStyle = isDark ? 'white' : '#303030';
context.strokeStyle = context.fillStyle;
context.lineWidth = 3;
// A speaker
context.beginPath();
context.moveTo(x + 6, y + 15);
context.lineTo(x + 13, y + 15);
context.lineTo(x + 22, y + 7);
context.lineTo(x + 22, y + 33);
context.lineTo(x + 13, y + 25);
context.lineTo(x + 6, y + 25);
context.closePath();
context.fill();
if (isMuted) {
// An X: the sound is off
context.beginPath();
context.moveTo(x + 27, y + 14);
context.lineTo(x + 37, y + 26);
context.moveTo(x + 37, y + 14);
context.lineTo(x + 27, y + 26);
context.stroke();
} else {
// Two waves: the sound is on
context.beginPath();
context.arc(x + 22, y + 20, 8, -0.8, 0.8);
context.stroke();
context.beginPath();
context.arc(x + 22, y + 20, 14, -0.8, 0.8);
context.stroke();
}
}
function drawText(text, x, y, font) {
context.font = font;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(text, x, y);
}
function drawStartScreen() {
// A light layer, so the text stands out from the bees
context.fillStyle = 'rgba(255, 255, 255, 0.6)';
context.fillRect(0, 0, canvasWidth, canvasHeight);
context.fillStyle = '#303030';
drawText('Pollen Rush', 375, 120, 'bold 48px Arial');
drawText(
'Collect the pollen. Dodge the bees.',
375, 175, '22px Arial'
);
drawText('Best: ' + highScore, 375, 320, '18px Arial');
// Fade the text in and out, once every second
let pulse = Math.sin(stateTime * 2 * Math.PI);
context.globalAlpha = 0.6 + 0.4 * pulse;
drawText('Click to start', 375, 270, '24px Arial');
context.globalAlpha = 1;
}
function drawPauseScreen() {
// Darken the game behind the menu
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
context.fillRect(0, 0, canvasWidth, canvasHeight);
context.fillStyle = 'white';
drawText('Paused', 375, 100, 'bold 40px Arial');
for (let i = 0; i < menuButtons.length; i++) {
drawButton(menuButtons[i]);
}
}
function drawButton(button) {
// Lighter when the mouse is over it
context.fillStyle = isOnButton(button)
? '#33b5c7'
: '#0099b0';
context.fillRect(
button.x, button.y, button.width, button.height
);
context.fillStyle = 'white';
drawText(
button.text,
button.x + button.width / 2, button.y + button.height / 2,
'22px Arial'
);
}
function drawGameOverScreen() {
// Darken the game behind the text
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
context.fillRect(0, 0, canvasWidth, canvasHeight);
context.fillStyle = 'white';
drawText('Game over', 375, 120, 'bold 48px Arial');
drawText(
'Score: ' + Math.floor(score), 375, 185, '28px Arial'
);
if (isNewHighScore) {
drawText('New best!', 375, 225, 'bold 22px Arial');
} else {
drawText('Best: ' + highScore, 375, 225, '22px Arial');
}
// A click only starts a new game after a second
if (stateTime > 1) {
drawText('Click to play again', 375, 300, '24px Arial');
}
}
function detectCollisions() {
let obj1;
let obj2;
// Reset the collision state of all objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].isColliding = false;
}
// Start checking for collisions
for (let i = 0; i < gameObjects.length; i++) {
obj1 = gameObjects[i];
for (let j = i + 1; j < gameObjects.length; j++) {
obj2 = gameObjects[j];
// Compare object1 with object2
if (!circleIntersect(
obj1.x, obj1.y, obj1.radius,
obj2.x, obj2.y, obj2.radius
)) {
continue;
}
obj1.isColliding = true;
obj2.isColliding = true;
// A bee that touches you costs a life
if (obj1 == player || obj2 == player) {
hitPlayer();
}
// Find the direction of the collision
let vCollision = {
x: obj2.x - obj1.x,
y: obj2.y - obj1.y
};
let distance = Math.sqrt(
(obj2.x - obj1.x) * (obj2.x - obj1.x) +
(obj2.y - obj1.y) * (obj2.y - obj1.y)
);
// Two objects in the same spot have no direction
if (distance == 0) {
continue;
}
let vCollisionNorm = {
x: vCollision.x / distance,
y: vCollision.y / distance
};
// Push them apart, so they don't sink into each other
separate(obj1, obj2, vCollisionNorm, distance);
// Calculate the speed of the detected collision
let vRelativeVelocity = {
x: obj1.vx - obj2.vx,
y: obj1.vy - obj2.vy
};
let speed =
vRelativeVelocity.x * vCollisionNorm.x +
vRelativeVelocity.y * vCollisionNorm.y;
// The objects are already moving apart
if (speed < 0) {
continue;
}
// Use the lower restitution of the two objects
let restitution = Math.min(
obj1.restitution, obj2.restitution
);
// Change the velocity of both objects
let totalMass = obj1.mass + obj2.mass;
let impulse = (1 + restitution) * speed / totalMass;
obj1.vx -= (impulse * obj2.mass * vCollisionNorm.x);
obj1.vy -= (impulse * obj2.mass * vCollisionNorm.y);
obj2.vx += (impulse * obj1.mass * vCollisionNorm.x);
obj2.vy += (impulse * obj1.mass * vCollisionNorm.y);
// Both bees feel the bump
obj1.bump();
obj2.bump();
// And you hear it, but only while you're playing
if (state == states.playing) {
playSound('bump', obj1.x);
}
}
}
}
function circleIntersect(x1, y1, r1, x2, y2, r2) {
// Calculate the squared distance between the two centers
let squareDistance =
(x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
// When the distance is less than or equal to the sum
// of the two radii, the circles touch or overlap
return squareDistance <= ((r1 + r2) * (r1 + r2));
}
function separate(obj1, obj2, vCollisionNorm, distance) {
// How far the circles overlap
let overlap = obj1.radius + obj2.radius - distance;
// Move them apart along the collision normal
let totalMass = obj1.mass + obj2.mass;
obj1.x -= vCollisionNorm.x * overlap * obj2.mass / totalMass;
obj1.y -= vCollisionNorm.y * overlap * obj2.mass / totalMass;
obj2.x += vCollisionNorm.x * overlap * obj1.mass / totalMass;
obj2.y += vCollisionNorm.y * overlap * obj1.mass / totalMass;
}
function detectEdgeCollisions() {
let obj;
for (let i = 0; i < gameObjects.length; i++) {
obj = gameObjects[i];
// Check for left and right
if (obj.x < obj.radius) {
obj.vx = Math.abs(obj.vx) * obj.restitution;
obj.x = obj.radius;
obj.bump();
} else if (obj.x > canvasWidth - obj.radius) {
obj.vx = -Math.abs(obj.vx) * obj.restitution;
obj.x = canvasWidth - obj.radius;
obj.bump();
}
// Check for top and bottom
if (obj.y < obj.radius) {
obj.vy = Math.abs(obj.vy) * obj.restitution;
obj.y = obj.radius;
obj.bump();
} else if (obj.y > canvasHeight - obj.radius) {
obj.vy = -Math.abs(obj.vy) * obj.restitution;
obj.y = canvasHeight - obj.radius;
obj.bump();
}
}
}
</script>
</body>
</html>The story behind this series
This series has been a long time in the making. The first three tutorials came out in March 2019. Over the next two years, four more followed, about the canvas, drawing, sprites and collisions. In 2026, we went over every part again, updated the code and the demos, and wrote the last four parts to finish the series. That's more than seven years, and a lot of evenings and weekends.
These tutorials are free to read, but the hosting isn't, and we pay for it ourselves. So if this series helped you, we'd really appreciate a donation.
What's next?
You've reached the end of the series. You started with an empty canvas, and now you have a game with sprites, controls, game states and sound, and you know how to get it out into the world.
Where do you go from here? Make Pollen Rush your own, with a new kind of enemy, a power-up or levels. Or try something new: make a flock of birds fly together, or use random numbers to make every round a little different.
If you have any questions, feel free to ask them in the comment section. And share your best Pollen Rush score while you're there! You can download the example code here.
If you'd like to support Spicy Yoghurt, please share our articles or follow us on social media.
Comments
Thanks! We've received your comment. We check every comment by hand before it appears, so this can take a few days.

No comments yet. Be the first to leave one!