|
(function() { |
|
|
|
'use strict'; |
|
|
|
var sling; |
|
var game; |
|
var BRIGHT_COLORS = ['#f44336', '#ff9800', '#ffeb3b', '#4caf50', '#2196f3', '#9c27b0']; |
|
var FIREWORKS_PROGRAM = [ |
|
20, 0, 0, 0, 40, 0, 0, 0, 60, 0, 0, 0, 80, 0, 0, 0, |
|
0, 0, 0, 20, 0, 0, 0, 80, 0, 0, 0, 40, 60, 0, 0, |
|
10, 20, 30, 40, 50, 60, 70, 80, 90, |
|
10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, |
|
90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, |
|
0, 0, 0, 0, 0, 0, 50 |
|
]; |
|
|
|
var Util = { |
|
randomBrightColor: function() { |
|
return BRIGHT_COLORS[this.randomInt(0, BRIGHT_COLORS.length)]; |
|
}, |
|
|
|
randomRGBA: function() { |
|
return [ |
|
'rgba(', [ |
|
this.randomInt(0, 255), |
|
this.randomInt(0, 255), |
|
this.randomInt(0, 255), |
|
Math.random() |
|
].join(','), |
|
')' |
|
].join(''); |
|
}, |
|
|
|
randomInt: function(min, max) { |
|
return Math.floor(Math.random() * (max - min + 1) + min); |
|
} |
|
}; |
|
|
|
var Sound = { |
|
play: function(url) { |
|
new Audio(url).play(); |
|
}, |
|
launch: function() { |
|
this.play('http://splatoperator.com/assets/swish.mp3'); |
|
}, |
|
explode: function() { |
|
this.play('http://splatoperator.com/assets/click.mp3'); |
|
}, |
|
ding: function () { |
|
this.play('http://splatoperator.com/assets/ding.mp3'); |
|
} |
|
}; |
|
|
|
function Game(width, height) { |
|
this.canvas = document.createElement('canvas'); |
|
this.canvas.width = width; |
|
this.canvas.height = height; |
|
this.canvas.style.position = 'absolute'; |
|
this.canvas.style.zIndex = 2147483647; |
|
this.canvas.style.pointerEvents = 'none'; |
|
this.canvas.style.overflow = 'hidden'; |
|
this.canvas.style.top = 0; |
|
this.canvas.style.left = 0; |
|
this.renderingContext = this.canvas.getContext('2d'); |
|
this.projectiles = []; |
|
this.particles = []; |
|
this.fireworkTimers = []; |
|
this.fireworkShowIsInProgress = false; |
|
|
|
document.body.appendChild(this.canvas); |
|
requestAnimationFrame(this.update.bind(this)); |
|
|
|
Sound.ding(); |
|
} |
|
|
|
Game.prototype.toggleFireworkShow = function() { |
|
this.fireworkShowIsInProgress = !this.fireworkShowIsInProgress; |
|
if (this.fireworkShowIsInProgress) { |
|
this.startFireworkShow(); |
|
} else { |
|
this.stopFireworkShow(); |
|
} |
|
}; |
|
|
|
Game.prototype.startFireworkShow = function() { |
|
var game = this; /* It's hard to hang onto `this` inside of .forEach */ |
|
this.fireworkTimers = []; |
|
|
|
FIREWORKS_PROGRAM.forEach(function(xPercentage, index) { |
|
if (xPercentage == 0) { |
|
return; /* Zeroes are for skipping this tick */ |
|
} |
|
|
|
game.fireworkTimers.push( |
|
setTimeout(function() { |
|
var x = (xPercentage / 100) * game.canvas.width; |
|
game.addProjectile(new Projectile({ |
|
x: x, |
|
y: game.height(), |
|
forceX: Util.randomInt(-20, 20), |
|
forceY: Util.randomInt(110, 130), |
|
fuse: Util.randomInt(40, 60) |
|
})); |
|
}, 250 * index) |
|
); |
|
}); |
|
game.fireworksTimers.push(setTimeout( |
|
game.stopFireworkShow, 250 * (FIREWORKS_PROGRAM.length) |
|
)); |
|
}; |
|
|
|
Game.prototype.stopFireworkShow = function() { |
|
var game = this; |
|
this.fireworkTimers.forEach(function(timer) { |
|
clearInterval(timer); |
|
}); |
|
this.fireworkTimers = []; |
|
}; |
|
|
|
Game.prototype.update = function() { |
|
//this.updateDimensions(); |
|
this.updateProjectiles(); |
|
this.updateParticles(); |
|
this.drawBackground(); |
|
this.drawProjectiles(); |
|
this.drawParticles(); |
|
requestAnimationFrame(this.update.bind(this)); |
|
}; |
|
|
|
Game.prototype.updateDimensions = function() { |
|
this.canvas.width = document.documentElement.clientWidth - 20; |
|
this.canvas.height = document.documentElement.clientHeight - 20; |
|
}; |
|
|
|
Game.prototype.updateThings = function(collection) { |
|
var game = this; |
|
/* XXX This looks funny because we're going through each collection twice, but |
|
* we need to prune dead things from the collection on a separate pass because |
|
* splicing the collection while we're iterating over it aborts the forEach. |
|
* */ |
|
collection.forEach(function(thing) { |
|
if (thing.dead) { |
|
game.removeThing(collection, thing); |
|
} |
|
}); |
|
|
|
collection.forEach(function(thing) { |
|
thing.update(); |
|
}); |
|
}; |
|
|
|
Game.prototype.updateProjectiles = function() { |
|
this.updateThings(this.projectiles); |
|
}; |
|
|
|
Game.prototype.updateParticles = function() { |
|
this.updateThings(this.particles); |
|
}; |
|
|
|
Game.prototype.drawBackground = function() { |
|
this.renderingContext.clearRect(0, 0, this.canvas.width, this.canvas.height); |
|
}; |
|
|
|
Game.prototype.drawCircles = function(collection) { |
|
var game = this; /* to access this game inside the forEach callback */ |
|
collection.forEach(function(item) { |
|
game.renderingContext.beginPath(); |
|
game.renderingContext.arc( |
|
item.position.x, |
|
item.position.y, |
|
Math.max(item.radius, 1), |
|
0, (2 * Math.PI), |
|
false |
|
); |
|
game.renderingContext.fillStyle = item.color; |
|
game.renderingContext.fill(); |
|
}); |
|
}; |
|
|
|
Game.prototype.drawParticles = function() { |
|
this.drawCircles(this.particles); |
|
}; |
|
|
|
Game.prototype.drawProjectiles = function() { |
|
this.drawCircles(this.projectiles); |
|
}; |
|
|
|
Game.prototype.addProjectile = function(projectile) { |
|
this.addThing(this.projectiles, projectile); |
|
}; |
|
|
|
Game.prototype.removeThing = function(collection, thing) { |
|
collection.splice(collection.indexOf(thing), 1); |
|
}; |
|
|
|
Game.prototype.width = function() { |
|
return document.documentElement.clientWidth; |
|
}; |
|
|
|
Game.prototype.height = function() { |
|
return document.documentElement.clientHeight; |
|
}; |
|
|
|
Game.prototype.addThing = function(collection, thing) { |
|
collection.push(thing); |
|
}; |
|
|
|
Game.prototype.addParticle = function(particle) { |
|
this.addThing(this.particles, particle); |
|
}; |
|
|
|
Game.prototype.detonateAllProjectiles = function() { |
|
this.projectiles.forEach(function(projectile) { |
|
projectile.explode(); |
|
}); |
|
}; |
|
|
|
function Sling(x, y) { |
|
this.startX = x; |
|
this.startY = y; |
|
document.addEventListener('mouseup', mouseup, true); |
|
} |
|
|
|
Sling.prototype.release = function(x, y) { |
|
var deltaX = (x - this.startX); |
|
var deltaY = (y - this.startY); |
|
game.addProjectile(new Projectile({ |
|
x: x, |
|
y: y, |
|
forceX: deltaX, |
|
forceY: deltaY |
|
})); |
|
}; |
|
|
|
function Projectile(params) { |
|
this.position = { |
|
x: params.x, |
|
y: params.y |
|
}; |
|
this.force = { |
|
x: params.forceX, |
|
y: params.forceY |
|
}; |
|
this.velocity = { |
|
x: -(this.force.x / 5), |
|
y: -(this.force.y / 3) |
|
}; |
|
this.fuse = params.fuse; |
|
this.radius = 5; |
|
this.gravity = 1; |
|
this.dead = false; |
|
this.color = '#00aedb'; |
|
|
|
Sound.launch(); |
|
} |
|
|
|
Projectile.prototype.outOfBounds = function() { |
|
/* Don't check whether it leaves the top of the viewport |
|
because that's fine. We want it to eventually fall back |
|
into the screen. */ |
|
return ( |
|
this.position.x < 0 || |
|
this.position.x > game.canvas.width || |
|
this.position.y > game.canvas.height |
|
); |
|
}; |
|
|
|
Projectile.prototype.update = function() { |
|
this.position.x += this.velocity.x; |
|
this.position.y += this.velocity.y; |
|
this.velocity.y += this.gravity; |
|
|
|
if (this.outOfBounds()) { |
|
this.explode(); |
|
} |
|
|
|
if (this.fuse !== undefined && this.fuse-- < 0) { |
|
this.explode(); |
|
} |
|
}; |
|
|
|
Projectile.prototype.explode = function() { |
|
Sound.explode(); |
|
|
|
for (var i = 0; i <= Util.randomInt(50, 100); i++) { |
|
game.addParticle(new ExplosionParticle(this.position.x, this.position.y)); |
|
} |
|
this.die(); |
|
}; |
|
|
|
Projectile.prototype.die = function() { |
|
this.dead = true; |
|
}; |
|
|
|
function ExplosionParticle(x, y) { |
|
this.position = { |
|
x: x, |
|
y: y |
|
}; |
|
this.velocity = { |
|
x: Util.randomInt(-10, 10), |
|
y: Util.randomInt(-10, 10) |
|
}; |
|
this.radius = Util.randomInt(2, 5); |
|
this.color = Util.randomBrightColor(); |
|
this.gravity = 0.5; |
|
this.dead = false; |
|
} |
|
|
|
ExplosionParticle.prototype.die = function() { |
|
this.dead = true; |
|
}; |
|
|
|
ExplosionParticle.prototype.update = function() { |
|
this.position.x = this.position.x < 0 ? 0 : this.position.x; |
|
this.position.y = this.position.y < 0 ? 0 : this.position.y; |
|
|
|
this.position.x += this.velocity.x; |
|
this.position.y += this.velocity.y; |
|
this.velocity.y += this.gravity; |
|
|
|
var shrink = setInterval(function() { |
|
this.radius--; |
|
}.bind(this), 500); |
|
|
|
if (this.radius < 1) { |
|
clearInterval(shrink); |
|
this.die(); |
|
} |
|
}; |
|
|
|
function mousedown(event) { |
|
sling = new Sling(event.clientX, event.clientY); |
|
} |
|
|
|
function mouseup(event) { |
|
sling.release(event.clientX, event.clientY); |
|
} |
|
|
|
function setup() { |
|
game = new Game(); |
|
|
|
window.addEventListener('resize', game.updateDimensions.bind(game)); |
|
game.updateDimensions(); |
|
|
|
document.addEventListener('keydown', game.detonateAllProjectiles.bind(game), true); |
|
document.addEventListener('mousedown', mousedown, true); |
|
document.addEventListener('keypress', function(event) { |
|
switch (event.keyCode) { |
|
case 102: |
|
game.toggleFireworkShow(); |
|
break; |
|
} |
|
}); |
|
} |
|
|
|
/* |
|
var previousKeys = []; |
|
var keyTimeout; |
|
document.addEventListener('keydown', function(event) { |
|
clearTimeout(keyTimeout); |
|
keyTimeout = setTimeout(function () { previousKeys = []; }, 2000); |
|
previousKeys.push(event.keyCode); |
|
if (previousKeys.toString() == '38,38,40,40,37,39,37,39,66') { |
|
setup(); |
|
} |
|
}); |
|
*/ |
|
|
|
setup(); |
|
}()); |