Skip to content

Instantly share code, notes, and snippets.

@aercolino
Last active February 6, 2020 15:37
Show Gist options
  • Save aercolino/31e1b464f1a31265787c7f8ad8d119fc to your computer and use it in GitHub Desktop.
Save aercolino/31e1b464f1a31265787c7f8ad8d119fc to your computer and use it in GitHub Desktop.
html{color:#000;background:#222222;}
a{cursor:pointer;}
html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}
table{border-collapse:collapse;border-spacing:0;}
fieldset,img{border:0;}
address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}
li{list-style:none;}
caption,th{text-align:left;}
/* h1,h2,h3,h4,h5,h6{font-size:100%;} */
q:before,q:after{content:'';}
abbr,acronym {border:0;font-variant:normal;}
sup {vertical-align:text-top;}
sub {vertical-align:text-bottom;}
input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;outline-style:none;outline-width:0pt;}
legend{color:#000;}
a:focus,object,h1,h2,h3,h4,h5,h6{-moz-outline-style: none; border:0px;}
/*input[type="Submit"]{cursor:pointer;}*/
strong {font-weight: bold;}
body {
overflow: hidden;
font-family: Helvetica, Arial, sans-serif;
color: #333333;
font-size: 11px;
background-color: #222222;
}
canvas {
cursor: crosshair;
z-index: 1;
}
canvas.background {
z-index: 0;
}
.ui {
font-family: Arial, Helvetica, sans-serif;
font-size: 10px;
color: #999999;
text-align: left;
padding: 8px;
background-color: rgba(0,0,0,0.5);
position: absolute;
z-index: 2;
margin: 0;
}
#message {
padding: 16px;
margin: 130px 0 0 0;
}
#status {
width: 100%;
height: 15px;
padding: 8px;
display: none;
}
#status .fps {
position: absolute;
right: 20px;
display: inline;
}
#status span {
color: #bbbbbb;
font-weight: bold;
margin-right: 5px;
}
#title {
margin-bottom: 20px;
color: #eeeeee;
}
.ui ul {
margin: 10px 0 10px 0;
}
.ui a {
outline: none;
font-family: Arial, Helvetica, sans-serif;
font-size: 38px;
text-decoration: none;
color: #bbbbbb;
padding: 2px;
display: block
}
.ui a:hover {
color: #ffffff;
background-color: #000000;
}
<canvas id='world'></canvas>
<canvas id='background-canvas' class="background"></canvas>
<div id="status" class="ui"></div>
<div id="message" class="ui">
<h2 id="title"></h2>
<ul>
<li>1. Protect the core.</li>
<li>2. Gather energy (green dots).</li>
<li>2. Hold SPACE for shield (depletes energy).</li>
</ul>
<a href="#" id="startButton">Start</a>
</div>
CoreWorld = new function(){
// The world dimensions
var SCREEN_WIDTH = window.innerWidth;
var SCREEN_HEIGHT = window.innerHeight;
// Types of organisms
var ORGANISM_ENEMY = 'enemy';
var ORGANISM_ENERGY = 'energy';
// Target framerate
var FRAMERATE = 60;
var canvas;
var context;
var canvasBackground;
var contextBackground;
// UI DOM elements
var status;
var message;
var title;
var startButton;
// Game elements
var organisms = [];
var particles = [];
var player;
// Mouse properties
var mouseX = (window.innerWidth - SCREEN_WIDTH);
var mouseY = (window.innerHeight - SCREEN_HEIGHT);
var mouseIsDown = false;
var spaceIsDown = false;
// Game properties and scoring
var playing = false;
var score = 0;
var time = 0;
var duration = 0;
var difficulty = 1;
var lastspawn = 0;
// The world's velocity
var velocity = { x: -1.3, y: 1 };
// Performance (FPS) tracking
var fps = 0;
var fpsMin = 1000;
var fpsMax = 0;
var timeLastSecond = new Date().getTime();
var frames = 0;
/**
* The SinuousWorld "constructor".
*/
this.init = function(){
canvas = document.getElementById('world');
canvasBackground = document.getElementById('background-canvas');
status = document.getElementById('status');
message = document.getElementById('message');
title = document.getElementById('title');
startButton = document.getElementById('startButton');
if (canvas && canvas.getContext) {
context = canvas.getContext('2d');
contextBackground = canvasBackground.getContext('2d');
// Register event listeners
document.addEventListener('mousemove', documentMouseMoveHandler, false);
document.addEventListener('mousedown', documentMouseDownHandler, false);
document.addEventListener('mouseup', documentMouseUpHandler, false);
canvas.addEventListener('touchstart', documentTouchStartHandler, false);
document.addEventListener('touchmove', documentTouchMoveHandler, false);
document.addEventListener('touchend', documentTouchEndHandler, false);
window.addEventListener('resize', windowResizeHandler, false);
startButton.addEventListener('click', startButtonClickHandler, false);
document.addEventListener('keydown', documentKeyDownHandler, false);
document.addEventListener('keyup', documentKeyUpHandler, false);
// Define our player
player = new Player();
// Force an initial resize to make sure the UI is sized correctly
windowResizeHandler();
setInterval(loop, 1000 / FRAMERATE);
renderBackground();
}
};
/**
*
*/
function renderBackground() {
var gradient = contextBackground.createRadialGradient( SCREEN_WIDTH * 0.5, SCREEN_HEIGHT * 0.5, 0, SCREEN_WIDTH * 0.5, SCREEN_HEIGHT * 0.5, 500 );
gradient.addColorStop(0,'rgba(0, 70, 70, 1)');
gradient.addColorStop(1,'rgba(0, 8, 14, 1)');
contextBackground.fillStyle = gradient;
contextBackground.fillRect( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT );
}
/**
* Handles click on the start button in the UI.
*/
function startButtonClickHandler(event){
if( playing == false ) {
playing = true;
// Reset game properties
organisms = [];
score = 0;
difficulty = 1;
// Reset the player data
player.energy = 30;
// Hide the game UI
message.style.display = 'none';
status.style.display = 'block';
time = new Date().getTime();
event.preventDefault();
}
}
/**
* Stops the currently ongoing game and shows the
* resulting data in the UI.
*/
function gameOver() {
playing = false;
// Determine the duration of the game
duration = new Date().getTime() - time;
// Show the UI
message.style.display = 'block';
// Ensure that the score is an integer
score = Math.round(score);
// Write the users score to the UI
title.innerHTML = 'Game Over! (' + score + ' points)';
// Update the status bar with the final score and time
scoreText = 'Score: <span>' + Math.round( score ) + '</span>';
scoreText += ' Time: <span>' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's</span>';
status.innerHTML = scoreText;
}
/**
*
*/
function documentKeyDownHandler(event) {
switch( event.keyCode ) {
case 32:
if( !spaceIsDown && player.energy > 15 ) {
player.energy -= 4;
}
spaceIsDown = true;
event.preventDefault();
break;
}
}
/**
*
*/
function documentKeyUpHandler(event) {
switch( event.keyCode ) {
case 32:
spaceIsDown = false;
event.preventDefault();
break;
}
}
/**
* Event handler for document.onmousemove.
*/
function documentMouseMoveHandler(event){
mouseX = event.clientX - (window.innerWidth - SCREEN_WIDTH) * 0.5 - 6;
mouseY = event.clientY - (window.innerHeight - SCREEN_HEIGHT) * 0.5 - 6;
}
/**
* Event handler for document.onmousedown.
*/
function documentMouseDownHandler(event){
mouseIsDown = true;
}
/**
* Event handler for document.onmouseup.
*/
function documentMouseUpHandler(event) {
mouseIsDown = false;
}
/**
* Event handler for document.ontouchstart.
*/
function documentTouchStartHandler(event) {
if(event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * 0.5;
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * 0.5;
mouseIsDown = true;
}
}
/**
* Event handler for document.ontouchmove.
*/
function documentTouchMoveHandler(event) {
if(event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * 0.5 - 60;
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * 0.5 - 30;
}
}
/**
* Event handler for document.ontouchend.
*/
function documentTouchEndHandler(event) {
mouseIsDown = false;
}
/**
* Event handler for window.onresize.
*/
function windowResizeHandler() {
// Update the game size
SCREEN_WIDTH = window.innerWidth;
SCREEN_HEIGHT = window.innerHeight;
// Center the player
player.position.x = SCREEN_WIDTH * 0.5;
player.position.y = SCREEN_HEIGHT * 0.5;
// Resize the canvas
canvas.width = SCREEN_WIDTH;
canvas.height = SCREEN_HEIGHT;
canvasBackground.width = SCREEN_WIDTH;
canvasBackground.height = SCREEN_HEIGHT;
// Determine the x/y position of the canvas
var cvx = (window.innerWidth - SCREEN_WIDTH) * 0.5;
var cvy = (window.innerHeight - SCREEN_HEIGHT) * 0.5;
// Position the canvas
canvas.style.position = 'absolute';
canvas.style.left = cvx + 'px';
canvas.style.top = cvy + 'px';
canvasBackground.style.position = 'absolute';
canvasBackground.style.left = cvx + 'px';
canvasBackground.style.top = cvy + 'px';
}
/**
* Emits a random number of particles from a set point.
*
* @param position The point where particles will be
* emitted from
* @param spread The pixel spread of the emittal
*/
function emitParticles( position, direction, spread, seed ) {
var q = seed + ( Math.random() * seed );
while( --q >= 0 ) {
var p = new Point();
p.position.x = position.x + ( Math.sin(q) * spread );
p.position.y = position.y + ( Math.cos(q) * spread );
p.velocity = { x: direction.x + ( -1 + Math.random() * 2 ), y: direction.y + ( - 1 + Math.random() * 2 ) };
p.alpha = 1;
particles.push( p );
}
}
/**
* Called on every frame to update the game properties
* and render the current state to the canvas.
*/
function loop() {
// Fetch the current time for this frame
var frameTime = new Date().getTime();
// Increase the frame count
frames ++;
// Check if a second has passed since the last time we updated the FPS
if( frameTime > timeLastSecond + 1000 ) {
// Establish the current, minimum and maximum FPS
fps = Math.min( Math.round( ( frames * 1000 ) / ( frameTime - timeLastSecond ) ), FRAMERATE );
fpsMin = Math.min( fpsMin, fps );
fpsMax = Math.max( fpsMax, fps );
timeLastSecond = frameTime;
frames = 0;
}
// A factor by which the score will be scaled, depending on current FPS
var scoreFactor = 0.01 + ( Math.max( Math.min( fps, FRAMERATE ), 0 ) / FRAMERATE * 0.99 );
// Scales down the factor by itself
scoreFactor = scoreFactor * scoreFactor;
// Clear the canvas of all old pixel data
context.clearRect(0,0,canvas.width,canvas.height);
var i, j, ilen, jlen;
// Only update game properties and draw the player if a game is active
if( playing ) {
// Increment the difficulty slightly
difficulty += 0.0015;
// Increment the score depending on difficulty
score += (0.4 * difficulty) * scoreFactor;
var targetAngle = Math.atan2( mouseY - player.position.y, mouseX - player.position.x );
if( Math.abs( targetAngle - player.angle ) > Math.PI ) {
player.angle = targetAngle;
}
player.angle += ( targetAngle - player.angle ) * 0.2;
player.energyRadiusTarget = ( player.energy / 100 ) * ( player.radius * 0.8 );
player.energyRadius += ( player.energyRadiusTarget - player.energyRadius ) * 0.2;
player.shield = { x: player.position.x + Math.cos( player.angle ) * player.radius, y: player.position.y + Math.sin( player.angle ) * player.radius };
// Shield
context.beginPath();
context.strokeStyle = '#648d93';
context.lineWidth = 3;
context.arc( player.position.x, player.position.y, player.radius, player.angle + 1.6, player.angle - 1.6, true );
context.stroke();
// Core
context.beginPath();
context.fillStyle = "#249d93";
context.strokeStyle = "#3be2d4";
context.lineWidth = 1.5;
player.updateCore();
var loopedNodes = player.coreNodes.concat();
loopedNodes.push( player.coreNodes[0] );
for( var i = 0; i < loopedNodes.length; i++ ) {
p = loopedNodes[i];
p2 = loopedNodes[i+1];
p.position.x += ( (player.position.x + p.normal.x + p.offset.x) - p.position.x ) * 0.2;
p.position.y += ( (player.position.y + p.normal.y + p.offset.y) - p.position.y ) * 0.2;
if( i == 0 ) {
// This is the first loop, so we need to start by moving into position
context.moveTo( p.position.x, p.position.y );
}
else if( p2 ) {
// Draw a curve between the current and next trail point
context.quadraticCurveTo( p.position.x, p.position.y, p.position.x + ( p2.position.x - p.position.x ) / 2, p.position.y + ( p2.position.y - p.position.y ) / 2 );
}
}
context.closePath();
context.fill();
context.stroke();
}
if( spaceIsDown && player.energy > 10 ) {
player.energy -= 0.1;
context.beginPath();
context.fillStyle = 'rgba( 0, 100, 100, ' + ( player.energy / 100 ) * 0.9 + ' )';
context.arc( player.position.x, player.position.y, player.radius, 0, Math.PI*2, true );
context.fill();
}
var enemyCount = 0;
var energyCount = 0;
// Go through each enemy and draw it + update its properties
for( i = 0; i < organisms.length; i++ ) {
p = organisms[i];
p.position.x += p.velocity.x;
p.position.y += p.velocity.y;
p.alpha += ( 1 - p.alpha ) * 0.1;
if( p.type == ORGANISM_ENEMY ) context.fillStyle = 'rgba( 255, 0, 0, ' + p.alpha + ' )';
if( p.type == ORGANISM_ENERGY ) context.fillStyle = 'rgba( 0, 235, 190, ' + p.alpha + ' )';
context.beginPath();
context.arc(p.position.x, p.position.y, p.size/2, 0, Math.PI*2, true);
context.fill();
var angle = Math.atan2( p.position.y - player.position.y, p.position.x - player.position.x );
if (playing) {
var dist = Math.abs( angle - player.angle );
if( dist > Math.PI ) {
dist = ( Math.PI * 2 ) - dist;
}
if ( dist < 1.6 ) {
if (p.distanceTo(player.position) > player.radius - 5 && p.distanceTo(player.position) < player.radius + 5) {
p.dead = true;
}
}
if (spaceIsDown && p.distanceTo(player.position) < player.radius && player.energy > 11) {
p.dead = true;
score += 4;
}
if (p.distanceTo(player.position) < player.energyRadius + (p.size * 0.5)) {
if (p.type == ORGANISM_ENEMY) {
player.energy -= 6;
}
if (p.type == ORGANISM_ENERGY) {
player.energy += 8;
score += 30;
}
player.energy = Math.max(Math.min(player.energy, 100), 0);
p.dead = true;
}
}
// If the enemy is outside of the game bounds, destroy it
if( p.position.x < -p.size || p.position.x > SCREEN_WIDTH + p.size || p.position.y < -p.size || p.position.y > SCREEN_HEIGHT + p.size ) {
p.dead = true;
}
// If the enemy is dead, remove it
if( p.dead ) {
emitParticles( p.position, { x: (p.position.x - player.position.x) * 0.02, y: (p.position.y - player.position.y) * 0.02 }, 5, 5 );
organisms.splice( i, 1 );
i --;
}
else {
if( p.type == ORGANISM_ENEMY ) enemyCount ++;
if( p.type == ORGANISM_ENERGY ) energyCount ++;
}
}
// If there are less enemies than intended for this difficulty, add another one
if( enemyCount < 1 * difficulty && new Date().getTime() - lastspawn > 100 ) {
organisms.push( giveLife( new Enemy() ) );
lastspawn = new Date().getTime();
}
//
if( energyCount < 1 && Math.random() > 0.996 ) {
organisms.push( giveLife( new Energy() ) );
}
// Go through and draw all particle effects
for( i = 0; i < particles.length; i++ ) {
p = particles[i];
// Apply velocity to the particle
p.position.x += p.velocity.x;
p.position.y += p.velocity.y;
// Fade out
p.alpha -= 0.02;
// Draw the particle
context.fillStyle = 'rgba(255,255,255,'+Math.max(p.alpha,0)+')';
context.fillRect( p.position.x, p.position.y, 1, 1 );
// If the particle is faded out to less than zero, remove it
if( p.alpha <= 0 ) {
particles.splice( i, 1 );
}
}
// If the game is active, update the game status bar with score, duration and FPS
if( playing ) {
scoreText = 'Score: <span>' + Math.round( score ) + '</span>';
scoreText += ' Time: <span>' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's</span>';
scoreText += ' <p class="fps">FPS: <span>' + Math.round( fps ) + ' ('+Math.round(Math.max(Math.min(fps/FRAMERATE, FRAMERATE), 0)*100)+'%)</span></p>';
status.innerHTML = scoreText;
if( player.energy === 0 ) {
emitParticles( player.position, { x: 0, y: 0 }, 10, 40 );
gameOver();
}
}
}
/**
*
*/
function giveLife( organism ) {
var side = Math.round( Math.random() * 3 );
switch( side ) {
case 0:
organism.position.x = 10;
organism.position.y = SCREEN_HEIGHT * Math.random();
break;
case 1:
organism.position.x = SCREEN_WIDTH * Math.random();
organism.position.y = 10;
break;
case 2:
organism.position.x = SCREEN_WIDTH - 10;
organism.position.y = SCREEN_HEIGHT * Math.random();
break;
case 3:
organism.position.x = SCREEN_WIDTH * Math.random();
organism.position.y = SCREEN_HEIGHT - 10;
break;
}
organism.speed = Math.min( Math.max( Math.random(), 0.6 ), 0.75 );
organism.velocity.x = ( player.position.x - organism.position.x ) * 0.006 * organism.speed;
organism.velocity.y = ( player.position.y - organism.position.y ) * 0.006 * organism.speed;
if( organism.type == 'enemy' ) {
organism.velocity.x *= (1+(Math.random()*0.1));
organism.velocity.y *= (1+(Math.random()*0.1));
}
organism.alpha = 0;
return organism;
}
};
/**
*
*/
function Point( x, y ) {
this.position = { x: x, y: y };
}
Point.prototype.distanceTo = function(p) {
var dx = p.x-this.position.x;
var dy = p.y-this.position.y;
return Math.sqrt(dx*dx + dy*dy);
};
Point.prototype.clonePosition = function() {
return { x: this.position.x, y: this.position.y };
};
/**
*
*/
function Player() {
this.position = { x: 0, y: 0 };
this.length = 15;
this.energy = 30;
this.energyRadius = 0;
this.energyRadiusTarget = 0;
this.radius = 60;
this.angle = 0;
this.coreQuality = 16;
this.coreNodes = [];
}
Player.prototype = new Point();
Player.prototype.updateCore = function() {
var i, j, n;
if( this.coreNodes.length == 0 ) {
var i, n;
for (i = 0; i < this.coreQuality; i++) {
n = {
position: { x: this.position.x, y: this.position.y },
normal: { x: 0, y: 0 },
normalTarget: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
};
this.coreNodes.push( n );
}
}
for (i = 0; i < this.coreQuality; i++) {
var n = this.coreNodes[i];
var angle = ( i / this.coreQuality ) * Math.PI * 2;
n.normal.x = Math.cos( angle ) * this.energyRadius;
n.normal.y = Math.sin( angle ) * this.energyRadius;
n.offset.x = Math.random() * 5;
n.offset.y = Math.random() * 5;
}
};
/**
*
*/
function Enemy() {
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.size = 6 + ( Math.random() * 4 );
this.speed = 1;
this.type = 'enemy';
}
Enemy.prototype = new Point();
/**
*
*/
function Energy() {
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.size = 10 + ( Math.random() * 6 );
this.speed = 1;
this.type = 'energy';
}
Energy.prototype = new Point();
CoreWorld.init();
html{color:#000;background:#222222;}
a{cursor:pointer;}
html,body,div,dl,dt,dd,ul,ol,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}
table{border-collapse:collapse;border-spacing:0;}
fieldset,img{border:0;}
address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}
caption,th{text-align:left;}
/* h1,h2,h3,h4,h5,h6{font-size:100%;} */
q:before,q:after{content:'';}
abbr,acronym {border:0;font-variant:normal;}
sup {vertical-align:text-top;}
sub {vertical-align:text-bottom;}
input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;outline-style:none;outline-width:0pt;}
legend{color:#000;}
a:focus,object,h1,h2,h3,h4,h5,h6{-moz-outline-style: none; border:0px;}
/*input[type="Submit"]{cursor:pointer;}*/
strong {font-weight: bold;}
body {
overflow: hidden;
font-family: Helvetica, Arial, sans-serif;
color: #333333;
font-size: 11px;
background-color: #222222;
}
canvas {
cursor: crosshair;
z-index: 1;
}
canvas.background {
z-index: 0;
}
.ui {
font-family: Arial, Helvetica, sans-serif;
font-size: 10px;
color: #999999;
text-align: left;
padding: 8px;
background-color: rgba(0,0,0,0.5);
position: absolute;
z-index: 2;
margin: 0;
}
#message {
padding: 16px;
margin: 130px 0 0 0;
}
#status {
width: 100%;
height: 15px;
padding: 8px;
display: none;
}
#status .fps {
position: absolute;
right: 20px;
display: inline;
}
#status span {
color: #bbbbbb;
font-weight: bold;
margin-right: 5px;
}
#title {
margin-bottom: 20px;
color: #eeeeee;
}
.ui ol {
margin: 10px 0 10px 10px;
}
.ui a {
outline: none;
font-family: Arial, Helvetica, sans-serif;
font-size: 38px;
text-decoration: none;
color: #bbbbbb;
padding: 2px;
display: block
}
.ui a:hover {
color: #ffffff;
background-color: #000000;
}
<canvas id="world" width="400" height="400"></canvas>
<canvas id="background-canvas" class="background" width="400" height="400"></canvas>
<div id="status" class="ui"></div>
<div id="message" class="ui">
<h2 id="title"></h2>
<ol>
<li>Move the mouse around for rotating the ship.</li>
<li>Gather energy (green dots).</li>
<li>Hold SPACE for firing bullets.</li>
</ol>
<a href="#" id="startButton">Start</a>
</div>
// forked from hakim's "Core" http://jsdo.it/hakim/core
function random( min, max ) {
var result = min + (max - min) * Math.random();
return result;
}
function randomInt( min, max ) {
// Math.round() gives a non-uniform distribution!
var result = min + Math.floor((max - min + 1) * Math.random());
return result;
}
var precision = 0.000000001;
function delta( num1, num2 ) {
var diff = num1 - num2;
var result = Math.abs(diff) < precision ? 0 : diff;
return result;
}
function equal( num1, num2 ) {
var result = delta(num1, num2) === 0;
return result;
}
function lesser( num1, num2 ) {
var result = num1 < num2 && ! equal(num1, num2);
return result;
}
function lesserOrEqual( num1, num2 ) {
var result = num1 < num2 || equal(num1, num2);
return result;
}
function between( num, min, max, equalMin, equalMax ) {
equalMin = typeof equalMin == 'undefined' ? true : !!equalMin;
equalMax = typeof equalMax == 'undefined' ? true : !!equalMax;
var result = true
&& (equalMin ? lesserOrEqual(min, num) : lesser(min, num))
&& (equalMax ? lesserOrEqual(num, max) : lesser(num, max));
return result;
}
function straightLine( point1, point2 ) {
//f(x,y) = a*x + b*y + c
var a = delta(point1.position.y, point2.position.y);
var b = delta(point2.position.x, point1.position.x);
var c = point1.position.x*point2.position.y - point2.position.x*point1.position.y;
function f( p ) {
return a*p.position.x + b*p.position.y + c;
}
//y = m*x + k
var isParallelToY = equal(b, 0);
var mX = isParallelToY ? Nan : -(a/b);
var kX = isParallelToY ? Nan : -(c/b);
function yX( p ) {
return mX*p.position.x + kX;
}
//x = m*y + k
var isParallelToX = equal(a, 0);
var mY = isParallelToX ? Nan : -(b/a);
var kY = isParallelToX ? Nan : -(c/a);
function xY( p ) {
return mY*p.position.y + kY;
}
var angle = isParallelToY ? Math.PI/2 : Math.atan(mX);
var cosAngle = Math.cos(angle);
var sinAngle = Math.sin(angle);
function compareParallel( pointP, axis, distance ) {
axis = axis != 'y' && axis != 'x' ? 'y' : axis;
distance = typeof distance == 'undefined' ? 0 : distance;
switch (axis) {
case 'x': //y is given
if (isParallelToX) return Nan;
var xYP = xY(pointP) + distance / Math.abs(sinAngle);
if (lesser(pointP.position.x, xYP))
return -1;
if (equal(pointP.position.x, xYP))
return 0;
return +1;
break;
case 'y': //x is given
if (isParallelToY) return Nan;
var yXP = yX(pointP) + distance / Math.abs(cosAngle);
if (lesser(pointP.position.y, yXP))
return -1;
if (equal(pointP.position.y, yXP))
return 0;
return +1;
break;
default:
break;
}
}
return {
implicit: function() {
return {a: a, b: b, c: c};
},
explicitX: function() {
return {m: mX, k: kX, isParallelToY: isParallelToY, x: point1.position.x};
},
explicitY: function() {
return {m: mY, k: kY, isParallelToX: isParallelToX, y: point1.position.y};
},
parallelAxis: function() {
return isParallelToX ? 'x' : (isParallelToY ? 'y': '');
},
contains: function( pointP ) {
return equal(f(pointP), 0);
},
compare: compareParallel
};
}
game = new function() {
//===============================================================READ ONLY==
var spaceIsDown = false;
this.isSpaceDown = function() {
return spaceIsDown;
};
var playing = false;
this.isPlaying = function() {
return playing;
};
var context = null;
this.getContext = function()
{
return context;
};
var ship = null;
this.getShip = function()
{
return ship;
};
var level = 1;
this.getLevel = function()
{
return level;
};
//==============================================================WRITE ONLY==
var score = 0;
this.incrementScore = function( value )
{
score += value;
};
var particles = [];
this.addParticles = function( position, spread, seed, direction ) {
var q = randomInt(seed, 2*seed);
while (--q >= 0) {
var p = new Particle(position, spread, q, direction);
particles.push(p);
}
};
//==================================================================PUBLIC==
this.init = function() {
canvas = document.getElementById('world');
canvasBackground = document.getElementById('background-canvas');
status = document.getElementById('status');
message = document.getElementById('message');
title = document.getElementById('title');
startButton = document.getElementById('startButton');
if (canvas && canvas.getContext) {
context = canvas.getContext('2d');
contextBackground = canvasBackground.getContext('2d');
document.addEventListener('mousemove', documentMouseMoveHandler, false);
canvas.addEventListener('touchstart', documentTouchStartHandler, false);
document.addEventListener('touchmove', documentTouchMoveHandler, false);
window.addEventListener('resize', windowResizeHandler, false);
startButton.addEventListener('click', startButtonClickHandler, false);
document.addEventListener('keydown', documentKeyDownHandler, false);
document.addEventListener('keyup', documentKeyUpHandler, false);
ship = new Ship(canvas.width/2, canvas.height/2);
//shield = new Shield(Math.PI/2);
windowResizeHandler();
renderBackground();
setInterval(loop, SECOND / FRAMERATE);
}
};
var bullets = [];
this.getBullets = function() {
return bullets.concat();
};
this.addBullets = function( position, direction ) {
var q = 2;
while (--q >= 0) {
var p = new Bullet(position, direction);
bullets.push(p);
}
};
//=================================================================PRIVATE==
var SECOND = 1000;
var FRAMERATE = 60;
var canvas;
var canvasBackground;
var contextBackground;
// UI DOM elements
var status;
var message;
var title;
var startButton;
// Mouse properties
var mouseX = 0;
var mouseY = 0;
var mouseIsDown = false;
// Game properties
var startTime = 0;
var frameTime = 0;
var organisms = [];
var prevEnemyTime = 0;
// Performance (FPS) tracking
var fps = 0;
var prevSecondTime = new Date().getTime();
var frames = 0;
function windowResizeHandler() {
var left = (window.innerWidth - canvas.width) / 2;
var top = (window.innerHeight - canvas.height) / 2;
canvas.style.position = 'absolute';
canvas.style.left = left + 'px';
canvas.style.top = top + 'px';
canvasBackground.style.position = 'absolute';
canvasBackground.style.left = left + 'px';
canvasBackground.style.top = top + 'px';
}
function renderBackground() {
var gradient = contextBackground.createRadialGradient(
canvas.width/2, canvas.height/2, 0,
canvas.width/2, canvas.height/2, 500);
gradient.addColorStop(0, 'rgba(0, 70, 70, 1)');
gradient.addColorStop(1, 'rgba(0, 8, 14, 1)');
contextBackground.fillStyle = gradient;
contextBackground.fillRect(0, 0, canvas.width, canvas.height);
}
//--------------------------------------------------------------------------
function documentKeyDownHandler( event ) {
switch (event.keyCode) {
case 32:
event.preventDefault();
spaceIsDown = true;
break;
default:
break;
}
}
function documentKeyUpHandler( event ) {
switch (event.keyCode) {
case 32:
event.preventDefault();
spaceIsDown = false;
break;
default:
break;
}
}
function documentMouseMoveHandler( event ) {
mouseX = event.clientX;
mouseY = event.clientY;
}
function documentTouchStartHandler( event ) {
if (event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX;
mouseY = event.touches[0].pageY;
}
}
function documentTouchMoveHandler( event ) {
if (event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX;
mouseY = event.touches[0].pageY;
}
}
function startButtonClickHandler( event ) {
event.preventDefault();
if (!playing) {
organisms = [];
score = 0;
level = 1;
ship.energy = 30;
message.style.display = 'none';
status.style.display = 'block';
startTime = new Date().getTime();
playing = true;
}
}
//--------------------------------------------------------------------------
function drawParticles() {
for (var i = 0, iTop = particles.length; i < iTop; i++) {
p = particles[i];
p.draw();
if (p.alpha === 0) {
particles.splice(i, 1);
i--;
iTop--;
}
}
}
function drawBullets() {
for (var i = 0, iTop = bullets.length; i < iTop; i++) {
p = bullets[i];
p.draw();
if (p.dead) {
bullets.splice(i, 1);
i--;
iTop--;
}
}
}
function drawOrganisms() {
var enemyCount = 0;
var energyCount = 0;
for (var i = 0, iTop = organisms.length; i < iTop; i++) {
p = organisms[i];
p.draw();
if (p.dead) {
p.explode();
organisms.splice(i, 1);
i--;
iTop--;
}
else {
if (p instanceof Enemy) enemyCount++;
if (p instanceof Energy) energyCount++;
}
}
if (enemyCount < level && frameTime - prevEnemyTime > 100) {
organisms.push(new Enemy());
prevEnemyTime = frameTime;
}
if (energyCount < 1 && Math.random() > 0.996) {
organisms.push(new Energy());
}
}
function currentFPS() {
frames++;
if (frameTime > prevSecondTime + SECOND) {
fps = frames / (frameTime - prevSecondTime) * SECOND;
prevSecondTime = frameTime;
frames = 0;
}
return fps;
}
/**
* http://www.mredkj.com/javascript/numberFormat.html
*/
function addCommas(nStr) {
nStr += '';
x = nStr.split('.');
x1 = x[0];
x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
}
function updateStatus() {
var duration = ((frameTime - startTime) / SECOND);
var text = 'Score: <span>' + addCommas(score.toFixed(0)) + '</span>';
text += ' Time: <span>' + duration.toFixed(2) + 's</span>';
text += ' Level: <span>' + level.toFixed(0) + '</span>';
if (playing) {
text += ' <p class="fps">FPS: <span>' + currentFPS().toFixed(0) + '</span></p>';
}
status.innerHTML = text;
}
function gameOver() {
playing = false;
updateStatus();
title.innerHTML = 'Game Over!';
message.style.display = 'block';
}
function loop() {
frameTime = new Date().getTime();
context.clearRect(0, 0, canvas.width, canvas.height);
if (playing) {
level += 0.0015;
score += 9 * level;
ship.draw(mouseX, mouseY);
if (spaceIsDown) {
ship.fire();
}
drawBullets();
updateStatus();
if (ship.energy === 0) {
ship.explode();
gameOver();
}
}
//draw organisms even if not playing, so that it works as a demo
drawOrganisms();
//draw partiles even if not playing, so that ship particles are drawn after game over
drawParticles();
}
};
//******************************************************************************
Point = Class.extend({
init: function( x, y ) {
this.context = game.getContext();
this.position = {
x: x || 0,
y: y || 0
};
},
distanceTo: function( p ) {
var dx = p.position.x - this.position.x;
var dy = p.position.y - this.position.y;
return Math.sqrt(dx * dx + dy * dy);
},
scale: function( factor ) {
factor = typeof factor == 'undefined' ? 1 : factor;
var xP = this.position.x;
var yP = this.position.y;
var xPS = xP * factor;
var yPS = yP * factor;
var result = new Point(xPS, yPS);
return result;
},
reverseScale: function( factor ) {
factor = typeof factor == 'undefined' ? 1 : factor;
var xPS = this.position.x;
var yPS = this.position.y;
var xP = xPS / factor;
var yP = yPS / factor;
var result = new Point(xP, yP);
return result;
},
rotate: function( theta ) {
theta = typeof theta == 'undefined' ? 0 : theta;
var sin = Math.sin(theta);
var cos = Math.cos(theta);
var xP = this.position.x;
var yP = this.position.y;
var xPR = xP * cos + yP * sin;
var yPR = -xP * sin + yP * cos;
var result = new Point(xPR, yPR);
return result;
},
reverseRotate: function( theta ) {
theta = typeof theta == 'undefined' ? 0 : theta;
var sin = Math.sin(theta);
var cos = Math.cos(theta);
var xPR = this.position.x;
var yPR = this.position.y;
var xP = xPR * cos - yPR * sin;
var yP = xPR * sin + yPR * cos;
var result = new Point(xP, yP);
return result;
},
translate: function( origin ) {
origin = origin || new Point(0, 0);
var xP = this.position.x;
var yP = this.position.y;
var xPT = xP - origin.position.x;
var yPT = yP - origin.position.y;
var result = new Point(xPT, yPT);
return result;
},
reverseTranslate: function( origin ) {
origin = origin || new Point(0, 0);
var xPT = this.position.x;
var yPT = this.position.y;
var xP = xPT + origin.position.x;
var yP = yPT + origin.position.y;
var result = new Point(xP, yP);
return result;
},
transformTRS: function( origin, theta, factor ) {
/*
var xP = pointP.position.x;
var yP = pointP.position.y;
origin = origin || new Point(0, 0);
var xPT = xP - origin.position.x;
var yPT = yP - origin.position.y;
theta = typeof theta == 'undefined' ? 0 : theta;
var sinTheta = Math.sin(theta);
var cosTheta = Math.cos(theta);
var xPTR = xPT * cosTheta + yPT * sinTheta;
var yPTR = -xPT * sinTheta + yPT * cosTheta;
factor = typeof factor == 'undefined' ? 1 : factor;
var xPTRS = factor * xPTR;
var yPTRS = factor * yPTR;
var result = new Point(xPTRS, yPTRS);
return result;
*/
var PT = this.translate(origin);
var PTR = PT.rotate(theta);
var PTRS = PTR.scale();
return PTRS;
},
reverseTransformTRS: function( origin, theta, factor ) {
var PTR = this.reverseScale(factor);
var PT = PTR.reverseRotate(theta);
var P = PT.reverseTranslate(origin);
return P;
}
});
//------------------------------------------------------------------------------
Ship = Point.extend({
init: function( x, y ) {
this._super(x, y);
this.energy = 60;
this.radius = 30;
this.angle1 = 1/5*Math.PI;
this.angle2 = 1/2*Math.PI;
var X1 = this.radius * Math.sin(this.angle1/2);
var X2 = this.radius * Math.sin(this.angle2/2);
var Y1 = this.radius * Math.cos(this.angle1/2);
var Y2 = this.radius * Math.cos(this.angle2/2);
this.pointA = new Point(- X1, - Y1);
this.pointB = new Point(+ X1, - Y1);
this.pointC = new Point(+ X2, + Y2);
this.pointD = new Point(- X2, + Y2);
this.angle = 0;
this.slDA = straightLine(this.pointD, this.pointA);
this.slBC = straightLine(this.pointB, this.pointC);
this.alpha = 0;
this.head = new Point(0, this.pointA.position.y);
},
draw: function(mouseX, mouseY) {
var targetAlpha = (this.energy / 100);
this.alpha += (targetAlpha - this.alpha) * 0.2;
var targetAngle = Math.atan2(mouseY - window.innerHeight/2, mouseX - window.innerWidth/2);
targetAngle += 1/2*Math.PI;
if (Math.abs(targetAngle - this.angle) > Math.PI) {
this.angle = targetAngle;
}
else {
this.angle += (targetAngle - this.angle) * 0.4;
}
this.context.save();
this.context.translate(this.position.x, this.position.y);
this.context.rotate(this.angle);
this.context.beginPath();
this.context.moveTo(this.pointA.position.x, this.pointA.position.y);
this.context.lineTo(this.pointB.position.x, this.pointB.position.y);
this.context.lineTo(this.pointC.position.x, this.pointC.position.y);
this.context.lineTo(this.pointD.position.x, this.pointD.position.y);
this.context.closePath();
this.context.fillStyle = 'rgba( 36, 157, 147, ' + this.alpha + ' )';
this.context.fill();
this.context.lineWidth = 1.5;
this.context.strokeStyle = "#3be2d4";
this.context.stroke();
this.context.restore();
},
increment: function( value ) {
this.energy = Math.min(Math.max(this.energy + value, 0), 100);
},
isHitting: function( organism ) {
var pointP = organism.transformTRS(this, this.angle);
var size = organism.size/2;
var result = between(pointP.position.y, this.pointA.position.y - size, this.pointD.position.y + size)
&& this.slDA.compare(pointP, 'x', -size) >= 0
&& this.slBC.compare(pointP, 'x', +size) <= 0;
return result;
},
explode: function() {
game.addParticles(this.position, 10, 40);
},
fire: function() {
var head = this.head.reverseTransformTRS(this, this.angle);
game.addBullets(this.position, {
x: (head.position.x - this.position.x) * 0.2,
y: (head.position.y - this.position.y) * 0.2
});
}
});
//------------------------------------------------------------------------------
Organism = Point.extend({
init: function() {
this.canvas = game.getContext().canvas;
this.ship = game.getShip();
var side = randomInt(0, 3);
switch (side) {
case 0:
this._super(10, randomInt(0, this.canvas.height));
break;
case 1:
this._super(randomInt(0, this.canvas.width), 10);
break;
case 2:
this._super(this.canvas.width - 10, randomInt(0, this.canvas.height));
break;
case 3:
this._super(randomInt(0, this.canvas.width), this.canvas.height - 10);
break;
default:
break;
}
var speed = 0.006 * random(0.6, 0.75);
this.velocity = {
x: (this.ship.position.x - this.position.x) * speed,
y: (this.ship.position.y - this.position.y) * speed
};
this.alpha = 0;
},
draw: function() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.incrementAlpha((1 - this.alpha) * 0.1);
this.context.beginPath();
this.context.fillStyle = this.fillStyle();
this.context.arc(this.position.x, this.position.y, this.size/2, 0, Math.PI*2);
this.context.fill();
this.checkAlive();
},
incrementAlpha: function( value ) {
this.alpha = Math.min(Math.max(this.alpha + value, 0), 1);
},
isLost: function() {
return false
|| this.position.x < 0 - this.size || this.position.x > this.canvas.width + this.size
|| this.position.y < 0 - this.size || this.position.y > this.canvas.height + this.size;
},
checkAlive: function() {
if (game.isPlaying()) {
if (this.ship.isHitting(this)) {
this.exchangeEnergy();
this.dead = true;
}
var bullets = game.getBullets();
for (var i = 0, iTop = bullets.length; i < iTop; i++) {
var p = bullets[i];
if (p.isHitting(this)) {
game.incrementScore(50);
this.dead = true;
}
}
}
if (this.isLost()) {
this.dead = true;
}
},
explode: function() {
game.addParticles(this.position, 5, 5, {
x: (this.position.x - this.ship.position.x) * 0.02,
y: (this.position.y - this.ship.position.y) * 0.02
});
}
});
//------------------------------------------------------------------------------
Enemy = Organism.extend({
init: function() {
this._super();
this.velocity.x *= random(1, 1.1);
this.velocity.y *= random(1, 1.1);
this.size = random(6, 10);
},
fillStyle: function() {
return 'rgba( 255, 0, 0, ' + this.alpha + ' )';
},
exchangeEnergy: function() {
game.getShip().increment(-6);
}
});
//------------------------------------------------------------------------------
Energy = Organism.extend({
init: function() {
this._super();
this.size = random(10, 16);
},
fillStyle: function() {
return 'rgba( 0, 235, 190, ' + this.alpha + ' )';
},
exchangeEnergy: function() {
game.getShip().increment(8);
game.incrementScore(30);
}
});
//------------------------------------------------------------------------------
Particle = Point.extend({
init: function( position, spread, q, direction ) {
this._super(
position.x + (Math.sin(q) * spread),
position.y + (Math.cos(q) * spread));
direction = typeof direction == 'undefined' ? {x: 0, y: 0} : direction;
this.velocity = {
x: direction.x + random(-1, 1),
y: direction.y + random(-1, 1)
};
this.alpha = 1;
},
draw: function() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.incrementAlpha(-0.02);
this.context.fillStyle = "#FFFFFF";
this.context.fillRect(this.position.x, this.position.y, 1, 1);
},
incrementAlpha: function( value ) {
this.alpha = Math.min(Math.max(this.alpha + value, 0), 1);
}
});
//------------------------------------------------------------------------------
Bullet = Point.extend({
init: function( position, direction ) {
this.canvas = game.getContext().canvas;
this._super(position.x, position.y);
this.velocity = {
x: direction.x * 2,
y: direction.y * 2
};
this.size = 2;
this.dead = false;
},
draw: function() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.context.fillStyle = 'rgba(255,255,255,1)';
this.context.fillRect(this.position.x, this.position.y, this.size, this.size);
this.checkAlive();
},
isLost: function() {
return false
|| this.position.x < 0 - this.size || this.position.x > this.canvas.width + this.size
|| this.position.y < 0 - this.size || this.position.y > this.canvas.height + this.size;
},
checkAlive: function() {
if (this.isLost()) {
this.dead = true;
}
},
isHitting: function( organism ) {
var result = this.distanceTo(organism) <= this.size/2 + organism.size/2;
return result;
}
});
//------------------------------------------------------------------------------
game.init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment