Skip to content

Instantly share code, notes, and snippets.

@creaturenex
Forked from insipx/index.html
Created March 21, 2023 00:09
Show Gist options
  • Save creaturenex/8ee679a5ce1503ad3fa776ce248c1243 to your computer and use it in GitHub Desktop.
Save creaturenex/8ee679a5ce1503ad3fa776ce248c1243 to your computer and use it in GitHub Desktop.
Space Invaders HTML5 Canvas
<link href='https://fonts.googleapis.com/css?family=Play:400,700' rel='stylesheet' type='text/css'>
<canvas id="game-canvas" width="640" height="640"></canvas>
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();
// ###################################################################
// shims
//
// ###################################################################
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
(function() {
if (!window.performance.now) {
window.performance.now = (!Date.now) ? function() { return new Date().getTime(); } :
function() { return Date.now(); }
}
})();
// ###################################################################
// Constants
//
// ###################################################################
var IS_CHROME = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
var CANVAS_WIDTH = 640;
var CANVAS_HEIGHT = 640;
var SPRITE_SHEET_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAEACAYAAAADRnAGAAACGUlEQVR42u3aSQ7CMBAEQIsn8P+/hiviAAK8zFIt5QbELiTHmfEYE3L9mZE9AAAAqAVwBQ8AAAD6THY5CgAAAKbfbPX3AQAAYBEEAADAuZrC6UUyfMEEAIBiAN8OePXnAQAAsLcmmKFPAQAAgHMbm+gbr3Sdo/LtcAAAANR6GywPAgBAM4D2JXAAABoBzBjA7AmlOx8AAEAzAOcDAADovTc4vQim6wUCABAYQG8QAADd4dPd2fRVYQAAANQG0B4HAABAawDnAwAA6AXgfAAAALpA2uMAAABwPgAAgPoAM9Ci/R4AAAD2dmqcEQIAIC/AiQGuAAYAAECcRS/a/cJXkUf2AAAAoBaA3iAAALrD+gIAAADY9baX/nwAAADNADwFAADo9YK0e5FMX/UFACA5QPSNEAAAAHKtCekmDAAAAADvBljtfgAAAGgMMGOrunvCy2uCAAAACFU6BwAAwF6AGQPa/XsAAADYB+B8AAAAtU+ItD4OAwAAAFVhAACaA0T7B44/BQAAANALwGMQAAAAADYO8If2+P31AgAAQN0SWbhFDwCAZlXgaO1xAAAA1FngnA8AACAeQPSNEAAAAM4CnC64AAAA4GzN4N9NSfgKEAAAAACszO26X8/X6BYAAAD0Anid8KcLAAAAAAAAAJBnwNEvAAAA9Jns1ygAAAAAAAAAAAAAAAAAAABAQ4COCENERERERERERBrnAa1sJuUVr3rsAAAAAElFTkSuQmCC';
var LEFT_KEY = 37;
var RIGHT_KEY = 39;
var SHOOT_KEY = 88;
var TEXT_BLINK_FREQ = 500;
var PLAYER_CLIP_RECT = { x: 0, y: 204, w: 62, h: 32 };
var ALIEN_BOTTOM_ROW = [ { x: 0, y: 0, w: 51, h: 34 }, { x: 0, y: 102, w: 51, h: 34 }];
var ALIEN_MIDDLE_ROW = [ { x: 0, y: 137, w: 50, h: 33 }, { x: 0, y: 170, w: 50, h: 34 }];
var ALIEN_TOP_ROW = [ { x: 0, y: 68, w: 50, h: 32 }, { x: 0, y: 34, w: 50, h: 32 }];
var ALIEN_X_MARGIN = 40;
var ALIEN_SQUAD_WIDTH = 11 * ALIEN_X_MARGIN;
// ###################################################################
// Utility functions & classes
//
// ###################################################################
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function clamp(num, min, max) {
return Math.min(Math.max(num, min), max);
}
function valueInRange(value, min, max) {
return (value <= max) && (value >= min);
}
function checkRectCollision(A, B) {
var xOverlap = valueInRange(A.x, B.x, B.x + B.w) ||
valueInRange(B.x, A.x, A.x + A.w);
var yOverlap = valueInRange(A.y, B.y, B.y + B.h) ||
valueInRange(B.y, A.y, A.y + A.h);
return xOverlap && yOverlap;
}
var Point2D = Class.extend({
init: function(x, y) {
this.x = (typeof x === 'undefined') ? 0 : x;
this.y = (typeof y === 'undefined') ? 0 : y;
},
set: function(x, y) {
this.x = x;
this.y = y;
}
});
var Rect = Class.extend({
init: function(x, y, w, h) {
this.x = (typeof x === 'undefined') ? 0 : x;
this.y = (typeof y === 'undefined') ? 0 : y;
this.w = (typeof w === 'undefined') ? 0 : w;
this.h = (typeof h === 'undefined') ? 0 : h;
},
set: function(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
});
// ###################################################################
// Globals
//
// ###################################################################
var canvas = null;
var ctx = null;
var spriteSheetImg = null;
var bulletImg = null;
var keyStates = null;
var prevKeyStates = null;
var lastTime = 0;
var player = null;
var aliens = [];
var particleManager = null;
var updateAlienLogic = false;
var alienDirection = -1;
var alienYDown = 0;
var alienCount = 0;
var wave = 1;
var hasGameStarted = false;
// ###################################################################
// Entities
//
// ###################################################################
var BaseSprite = Class.extend({
init: function(img, x, y) {
this.img = img;
this.position = new Point2D(x, y);
this.scale = new Point2D(1, 1);
this.bounds = new Rect(x, y, this.img.width, this.img.height);
this.doLogic = true;
},
update: function(dt) { },
_updateBounds: function() {
this.bounds.set(this.position.x, this.position.y, ~~(0.5 + this.img.width * this.scale.x), ~~(0.5 + this.img.height * this.scale.y));
},
_drawImage: function() {
ctx.drawImage(this.img, this.position.x, this.position.y);
},
draw: function(resized) {
this._updateBounds();
this._drawImage();
}
});
var SheetSprite = BaseSprite.extend({
init: function(sheetImg, clipRect, x, y) {
this._super(sheetImg, x, y);
this.clipRect = clipRect;
this.bounds.set(x, y, this.clipRect.w, this.clipRect.h);
},
update: function(dt) {},
_updateBounds: function() {
var w = ~~(0.5 + this.clipRect.w * this.scale.x);
var h = ~~(0.5 + this.clipRect.h * this.scale.y);
this.bounds.set(this.position.x - w/2, this.position.y - h/2, w, h);
},
_drawImage: function() {
ctx.save();
ctx.transform(this.scale.x, 0, 0, this.scale.y, this.position.x, this.position.y);
ctx.drawImage(this.img, this.clipRect.x, this.clipRect.y, this.clipRect.w, this.clipRect.h, ~~(0.5 + -this.clipRect.w*0.5), ~~(0.5 + -this.clipRect.h*0.5), this.clipRect.w, this.clipRect.h);
ctx.restore();
},
draw: function(resized) {
this._super(resized);
}
});
var Player = SheetSprite.extend({
init: function() {
this._super(spriteSheetImg, PLAYER_CLIP_RECT, CANVAS_WIDTH/2, CANVAS_HEIGHT - 70);
this.scale.set(0.85, 0.85);
this.lives = 3;
this.xVel = 0;
this.bullets = [];
this.bulletDelayAccumulator = 0;
this.score = 0;
},
reset: function() {
this.lives = 3;
this.score = 0;
this.position.set(CANVAS_WIDTH/2, CANVAS_HEIGHT - 70);
},
shoot: function() {
var bullet = new Bullet(this.position.x, this.position.y - this.bounds.h / 2, 1, 1000);
this.bullets.push(bullet);
},
handleInput: function() {
if (isKeyDown(LEFT_KEY)) {
this.xVel = -175;
} else if (isKeyDown(RIGHT_KEY)) {
this.xVel = 175;
} else this.xVel = 0;
if (wasKeyPressed(SHOOT_KEY)) {
if (this.bulletDelayAccumulator > 0.5) {
this.shoot();
this.bulletDelayAccumulator = 0;
}
}
},
updateBullets: function(dt) {
for (var i = this.bullets.length - 1; i >= 0; i--) {
var bullet = this.bullets[i];
if (bullet.alive) {
bullet.update(dt);
} else {
this.bullets.splice(i, 1);
bullet = null;
}
}
},
update: function(dt) {
// update time passed between shots
this.bulletDelayAccumulator += dt;
// apply x vel
this.position.x += this.xVel * dt;
// cap player position in screen bounds
this.position.x = clamp(this.position.x, this.bounds.w/2, CANVAS_WIDTH - this.bounds.w/2);
this.updateBullets(dt);
},
draw: function(resized) {
this._super(resized);
// draw bullets
for (var i = 0, len = this.bullets.length; i < len; i++) {
var bullet = this.bullets[i];
if (bullet.alive) {
bullet.draw(resized);
}
}
}
});
var Bullet = BaseSprite.extend({
init: function(x, y, direction, speed) {
this._super(bulletImg, x, y);
this.direction = direction;
this.speed = speed;
this.alive = true;
},
update: function(dt) {
this.position.y -= (this.speed * this.direction) * dt;
if (this.position.y < 0) {
this.alive = false;
}
},
draw: function(resized) {
this._super(resized);
}
});
var Enemy = SheetSprite.extend({
init: function(clipRects, x, y) {
this._super(spriteSheetImg, clipRects[0], x, y);
this.clipRects = clipRects;
this.scale.set(0.5, 0.5);
this.alive = true;
this.onFirstState = true;
this.stepDelay = 1; // try 2 secs to start with...
this.stepAccumulator = 0;
this.doShoot - false;
this.bullet = null;
},
toggleFrame: function() {
this.onFirstState = !this.onFirstState;
this.clipRect = (this.onFirstState) ? this.clipRects[0] : this.clipRects[1];
},
shoot: function() {
this.bullet = new Bullet(this.position.x, this.position.y + this.bounds.w/2, -1, 500);
},
update: function(dt) {
this.stepAccumulator += dt;
if (this.stepAccumulator >= this.stepDelay) {
if (this.position.x < this.bounds.w/2 + 20 && alienDirection < 0) {
updateAlienLogic = true;
} if (alienDirection === 1 && this.position.x > CANVAS_WIDTH - this.bounds.w/2 - 20) {
updateAlienLogic = true;
}
if (this.position.y > CANVAS_WIDTH - 50) {
reset();
}
var fireTest = Math.floor(Math.random() * (this.stepDelay + 1));
if (getRandomArbitrary(0, 1000) <= 5 * (this.stepDelay + 1)) {
this.doShoot = true;
}
this.position.x += 10 * alienDirection;
this.toggleFrame();
this.stepAccumulator = 0;
}
this.position.y += alienYDown;
if (this.bullet !== null && this.bullet.alive) {
this.bullet.update(dt);
} else {
this.bullet = null;
}
},
draw: function(resized) {
this._super(resized);
if (this.bullet !== null && this.bullet.alive) {
this.bullet.draw(resized);
}
}
});
var ParticleExplosion = Class.extend({
init: function() {
this.particlePool = [];
this.particles = [];
},
draw: function() {
for (var i = this.particles.length - 1; i >= 0; i--) {
var particle = this.particles[i];
particle.moves++;
particle.x += particle.xunits;
particle.y += particle.yunits + (particle.gravity * particle.moves);
particle.life--;
if (particle.life <= 0 ) {
if (this.particlePool.length < 100) {
this.particlePool.push(this.particles.splice(i,1));
} else {
this.particles.splice(i,1);
}
} else {
ctx.globalAlpha = (particle.life)/(particle.maxLife);
ctx.fillStyle = particle.color;
ctx.fillRect(particle.x, particle.y, particle.width, particle.height);
ctx.globalAlpha = 1;
}
}
},
createExplosion: function(x, y, color, number, width, height, spd, grav, lif) {
for (var i =0;i < number;i++) {
var angle = Math.floor(Math.random()*360);
var speed = Math.floor(Math.random()*spd/2) + spd;
var life = Math.floor(Math.random()*lif)+lif/2;
var radians = angle * Math.PI/ 180;
var xunits = Math.cos(radians) * speed;
var yunits = Math.sin(radians) * speed;
if (this.particlePool.length > 0) {
var tempParticle = this.particlePool.pop();
tempParticle.x = x;
tempParticle.y = y;
tempParticle.xunits = xunits;
tempParticle.yunits = yunits;
tempParticle.life = life;
tempParticle.color = color;
tempParticle.width = width;
tempParticle.height = height;
tempParticle.gravity = grav;
tempParticle.moves = 0;
tempParticle.alpha = 1;
tempParticle.maxLife = life;
this.particles.push(tempParticle);
} else {
this.particles.push({x:x,y:y,xunits:xunits,yunits:yunits,life:life,color:color,width:width,height:height,gravity:grav,moves:0,alpha:1, maxLife:life});
}
}
}
});
// ###################################################################
// Initialization functions
//
// ###################################################################
function initCanvas() {
// create our canvas and context
canvas = document.getElementById('game-canvas');
ctx = canvas.getContext('2d');
// turn off image smoothing
setImageSmoothing(false);
// create our main sprite sheet img
spriteSheetImg = new Image();
spriteSheetImg.src = SPRITE_SHEET_SRC;
preDrawImages();
// add event listeners and initially resize
window.addEventListener('resize', resize);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
}
function preDrawImages() {
var canvas = drawIntoCanvas(2, 8, function(ctx) {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
});
bulletImg = new Image();
bulletImg.src = canvas.toDataURL();
}
function setImageSmoothing(value) {
this.ctx['imageSmoothingEnabled'] = value;
this.ctx['mozImageSmoothingEnabled'] = value;
this.ctx['oImageSmoothingEnabled'] = value;
this.ctx['webkitImageSmoothingEnabled'] = value;
this.ctx['msImageSmoothingEnabled'] = value;
}
function initGame() {
dirtyRects = [];
aliens = [];
player = new Player();
particleManager = new ParticleExplosion();
setupAlienFormation();
drawBottomHud();
}
function setupAlienFormation() {
alienCount = 0;
for (var i = 0, len = 5 * 11; i < len; i++) {
var gridX = (i % 11);
var gridY = Math.floor(i / 11);
var clipRects;
switch (gridY) {
case 0:
case 1: clipRects = ALIEN_BOTTOM_ROW; break;
case 2:
case 3: clipRects = ALIEN_MIDDLE_ROW; break;
case 4: clipRects = ALIEN_TOP_ROW; break;
}
aliens.push(new Enemy(clipRects, (CANVAS_WIDTH/2 - ALIEN_SQUAD_WIDTH/2) + ALIEN_X_MARGIN/2 + gridX * ALIEN_X_MARGIN, CANVAS_HEIGHT/3.25 - gridY * 40));
alienCount++;
}
}
function reset() {
aliens = [];
setupAlienFormation();
player.reset();
}
function init() {
initCanvas();
keyStates = [];
prevKeyStates = [];
resize();
}
// ###################################################################
// Helpful input functions
//
// ###################################################################
function isKeyDown(key) {
return keyStates[key];
}
function wasKeyPressed(key) {
return !prevKeyStates[key] && keyStates[key];
}
// ###################################################################
// Drawing & Update functions
//
// ###################################################################
function updateAliens(dt) {
if (updateAlienLogic) {
updateAlienLogic = false;
alienDirection = -alienDirection;
alienYDown = 25;
}
for (var i = aliens.length - 1; i >= 0; i--) {
var alien = aliens[i];
if (!alien.alive) {
aliens.splice(i, 1);
alien = null;
alienCount--;
if (alienCount < 1) {
wave++;
setupAlienFormation();
}
return;
}
alien.stepDelay = ((alienCount * 20) - (wave * 10)) / 1000;
if (alien.stepDelay <= 0.05) {
alien.stepDelay = 0.05;
}
alien.update(dt);
if (alien.doShoot) {
alien.doShoot = false;
alien.shoot();
}
}
alienYDown = 0;
}
function resolveBulletEnemyCollisions() {
var bullets = player.bullets;
for (var i = 0, len = bullets.length; i < len; i++) {
var bullet = bullets[i];
for (var j = 0, alen = aliens.length; j < alen; j++) {
var alien = aliens[j];
if (checkRectCollision(bullet.bounds, alien.bounds)) {
alien.alive = bullet.alive = false;
particleManager.createExplosion(alien.position.x, alien.position.y, 'white', 70, 5,5,3,.15,50);
player.score += 25;
}
}
}
}
function resolveBulletPlayerCollisions() {
for (var i = 0, len = aliens.length; i < len; i++) {
var alien = aliens[i];
if (alien.bullet !== null && checkRectCollision(alien.bullet.bounds, player.bounds)) {
if (player.lives === 0) {
hasGameStarted = false;
} else {
alien.bullet.alive = false;
particleManager.createExplosion(player.position.x, player.position.y, 'green', 100, 8,8,6,0.001,40);
player.position.set(CANVAS_WIDTH/2, CANVAS_HEIGHT - 70);
player.lives--;
break;
}
}
}
}
function resolveCollisions() {
resolveBulletEnemyCollisions();
resolveBulletPlayerCollisions();
}
function updateGame(dt) {
player.handleInput();
prevKeyStates = keyStates.slice();
player.update(dt);
updateAliens(dt);
resolveCollisions();
}
function drawIntoCanvas(width, height, drawFunc) {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
drawFunc(ctx);
return canvas;
}
function fillText(text, x, y, color, fontSize) {
if (typeof color !== 'undefined') ctx.fillStyle = color;
if (typeof fontSize !== 'undefined') ctx.font = fontSize + 'px Play';
ctx.fillText(text, x, y);
}
function fillCenteredText(text, x, y, color, fontSize) {
var metrics = ctx.measureText(text);
fillText(text, x - metrics.width/2, y, color, fontSize);
}
function fillBlinkingText(text, x, y, blinkFreq, color, fontSize) {
if (~~(0.5 + Date.now() / blinkFreq) % 2) {
fillCenteredText(text, x, y, color, fontSize);
}
}
function drawBottomHud() {
ctx.fillStyle = '#02ff12';
ctx.fillRect(0, CANVAS_HEIGHT - 30, CANVAS_WIDTH, 2);
fillText(player.lives + ' x ', 10, CANVAS_HEIGHT - 7.5, 'white', 20);
ctx.drawImage(spriteSheetImg, player.clipRect.x, player.clipRect.y, player.clipRect.w,
player.clipRect.h, 45, CANVAS_HEIGHT - 23, player.clipRect.w * 0.5,
player.clipRect.h * 0.5);
fillText('CREDIT: ', CANVAS_WIDTH - 115, CANVAS_HEIGHT - 7.5);
fillCenteredText('SCORE: ' + player.score, CANVAS_WIDTH/2, 20);
fillBlinkingText('00', CANVAS_WIDTH - 25, CANVAS_HEIGHT - 7.5, TEXT_BLINK_FREQ);
}
function drawAliens(resized) {
for (var i = 0; i < aliens.length; i++) {
var alien = aliens[i];
alien.draw(resized);
}
}
function drawGame(resized) {
player.draw(resized);
drawAliens(resized);
particleManager.draw();
drawBottomHud();
}
function drawStartScreen() {
fillCenteredText("Space Invaders", CANVAS_WIDTH/2, CANVAS_HEIGHT/2.75, '#FFFFFF', 36);
fillBlinkingText("Press enter to play!", CANVAS_WIDTH/2, CANVAS_HEIGHT/2, 500, '#FFFFFF', 36);
}
function animate() {
var now = window.performance.now();
var dt = now - lastTime;
if (dt > 100) dt = 100;
if (wasKeyPressed(13) && !hasGameStarted) {
initGame();
hasGameStarted = true;
}
if (hasGameStarted) {
updateGame(dt / 1000);
}
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
if (hasGameStarted) {
drawGame(false);
} else {
drawStartScreen();
}
lastTime = now;
requestAnimationFrame(animate);
}
// ###################################################################
// Event Listener functions
//
// ###################################################################
function resize() {
var w = window.innerWidth;
var h = window.innerHeight;
// calculate the scale factor to keep a correct aspect ratio
var scaleFactor = Math.min(w / CANVAS_WIDTH, h / CANVAS_HEIGHT);
if (IS_CHROME) {
canvas.width = CANVAS_WIDTH * scaleFactor;
canvas.height = CANVAS_HEIGHT * scaleFactor;
setImageSmoothing(false);
ctx.transform(scaleFactor, 0, 0, scaleFactor, 0, 0);
} else {
// resize the canvas css properties
canvas.style.width = CANVAS_WIDTH * scaleFactor + 'px';
canvas.style.height = CANVAS_HEIGHT * scaleFactor + 'px';
}
}
function onKeyDown(e) {
e.preventDefault();
keyStates[e.keyCode] = true;
}
function onKeyUp(e) {
e.preventDefault();
keyStates[e.keyCode] = false;
}
// ###################################################################
// Start game!
//
// ###################################################################
window.onload = function() {
init();
animate();
};
body, html{
background-color: #EEEEEE;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
margin: auto;
position :absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment